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,1783 @@
|
|
|
1
|
+
"""Inspection view functions and supporting definitions."""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from meltygui.core.melty import Melty
|
|
4
|
+
from meltygui.core.melty import SearchTerm
|
|
5
|
+
from meltygui.core.rendering.modes import Modes
|
|
6
|
+
from meltygui.core.core_render import render_func
|
|
7
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
8
|
+
from meltygui.core.rendering.window_decoration import window
|
|
9
|
+
from meltygui.core.rendering.render_funcs import RenderFuncs
|
|
10
|
+
from meltygui.state.inspection_state import ContextMenuState
|
|
11
|
+
from meltygui.state.inspection_state import _InfoRow
|
|
12
|
+
from meltygui.state.new_core_model import ContextMenuWindowState
|
|
13
|
+
from meltygui.state.new_core_model import TabState
|
|
14
|
+
from meltygui.core.runtime.toggles import Tint
|
|
15
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
16
|
+
from meltygui.core.runtime.toggles import hsv_to_rgb
|
|
17
|
+
from meltygui.core.runtime.toggles import rgb_to_hsv
|
|
18
|
+
import inspect
|
|
19
|
+
import meltygui_imgui as imgui
|
|
20
|
+
import threading
|
|
21
|
+
import types
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@render_func(use_cache=False, show_bg=False, disable_scroll=True, shadow=False, selectable=False)
|
|
25
|
+
def draw_with_modes(input_value, modes, tab_state: TabState = None, search_text="", draw_state=None, unique=0):
|
|
26
|
+
from meltygui.view.tab_view import draw_tab_bar
|
|
27
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
28
|
+
from meltygui.core.rendering.render_dispatch import input_tab_name
|
|
29
|
+
from meltygui.core.rendering.render_dispatch import tab_names
|
|
30
|
+
|
|
31
|
+
if not tab_state.selected_tabs:
|
|
32
|
+
tab_state.selected_tabs = [modes[0]]
|
|
33
|
+
imgui.dummy(0, 5)
|
|
34
|
+
tint_value = 0.0
|
|
35
|
+
tint_saturation = 0.688
|
|
36
|
+
tab_changed, new_tabs = draw_tab_bar(input_value=tab_state.selected_tabs,
|
|
37
|
+
tab_height=30, show_bg=False, bg_offset=1,
|
|
38
|
+
name=f"tab_bar{unique}", wrap=True,
|
|
39
|
+
collection=modes, as_toggles=False)
|
|
40
|
+
if tab_changed:
|
|
41
|
+
tab_state.selected_tabs = new_tabs
|
|
42
|
+
# The compact Info tab's initial fit leaves no room for source rows.
|
|
43
|
+
# Give Inputs a usable viewport when entering it from that small fit.
|
|
44
|
+
if tab_names.index(input_tab_name) in new_tabs:
|
|
45
|
+
minimum_height = min(700, int(imgui.get_io().display_size.y * 0.8))
|
|
46
|
+
if (draw_state.height or 0) < minimum_height:
|
|
47
|
+
draw_state.height = minimum_height
|
|
48
|
+
draw_state.invalidate()
|
|
49
|
+
|
|
50
|
+
imgui.dummy(0, 2)
|
|
51
|
+
changed = False
|
|
52
|
+
value = input_value
|
|
53
|
+
for idx, mode in enumerate(tab_state.selected_tabs):
|
|
54
|
+
mode_changed, value = draw_any(input_value, name=f"Mode: {mode} {unique}", mode=mode, selectable=False,
|
|
55
|
+
show_name=False,
|
|
56
|
+
with_header=None, show_header=False, disable_scroll=False,
|
|
57
|
+
indent_size=0, show_bg=False, use_cache=True, shadow=False, column=idx)
|
|
58
|
+
changed |= mode_changed
|
|
59
|
+
|
|
60
|
+
return changed, value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@render_func(tint=(0.18, 0.32, 0.55), use_cache=False, show_name=False, show_bg=False)
|
|
64
|
+
def draw_view_func_selector(input_value, search_text="", draw_state=None, **kwargs):
|
|
65
|
+
"""Select a registered renderer; the caller owns applying the choice."""
|
|
66
|
+
from meltygui.view.dropdown_view import draw_dropdown
|
|
67
|
+
from meltygui.model.search_model import _fuzzy_key_match
|
|
68
|
+
|
|
69
|
+
from meltygui.core.input.view_selection import view_func_name
|
|
70
|
+
choices = {name: getattr(RenderFuncs, name)
|
|
71
|
+
for name in sorted(Melty.render_funcs_by_name)
|
|
72
|
+
if not search_text or _fuzzy_key_match(search_text.lower(), name.lower())}
|
|
73
|
+
options = dict(kwargs)
|
|
74
|
+
options.pop("view_func", None)
|
|
75
|
+
options["use_cache"] = False
|
|
76
|
+
options["display_label"] = view_func_name(input_value)
|
|
77
|
+
changed, selected = draw_dropdown(input_value, collection=choices, **options)
|
|
78
|
+
return changed, selected if changed else input_value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@render_func(use_cache=False, show_bg=False, shadow=False, with_header=None,
|
|
82
|
+
show_name=False, selectable=False, is_tree=True, temp=True, searchable=False)
|
|
83
|
+
def draw_param_matrix(input_value, wrap=True, search_text="", draw_state=None, source_tints=None, unique=None,
|
|
84
|
+
source_locations=None, priority_params=(), source_order=(), view_draw_state=None,
|
|
85
|
+
source_dicts=None, writable_sources=(), source_kinds=None, **kwargs):
|
|
86
|
+
"""The inputs-tab parameter screen: ONE parameter at a time, EVERY source.
|
|
87
|
+
A dropdown at the top switches between all the parameters identified for
|
|
88
|
+
the view (its own signature params first, then the @render_func machinery
|
|
89
|
+
kwargs); below it, one row per possible input source — param default
|
|
90
|
+
(signature), caller, mode, class @defaults, function decoration — in
|
|
91
|
+
`source_order`, shown whether or not the source currently sets the value.
|
|
92
|
+
Sources that set the param show their editable value; the rest show a dim
|
|
93
|
+
"not set", so the parameter's full input surface is mapped in one glance.
|
|
94
|
+
|
|
95
|
+
Cells are parse FRAGMENTS (leaves pulled out of their codec's parse), so
|
|
96
|
+
they can't naturally adopt the codec tint the way a whole codec-typed
|
|
97
|
+
value does — this view is special: it looks the tint up per source (the
|
|
98
|
+
tab maps each row to its codec) and applies it MANUALLY, alpha-boosted,
|
|
99
|
+
so the data source is highly visible at a glance. Cell edits mutate the
|
|
100
|
+
row in place and report changed, for apply_param_source_matrix write-back.
|
|
101
|
+
|
|
102
|
+
`priority_params` (see signature_param_names) orders the view function's
|
|
103
|
+
own signature params to the front of the dropdown list.
|
|
104
|
+
|
|
105
|
+
`search_text` (the tab's Ctrl+F find bar) filters the dropdown's param
|
|
106
|
+
list and auto-switches the screen to the best match: exact substring for
|
|
107
|
+
short terms, the shared typo-tolerant matcher (_fuzzy_key_match) for
|
|
108
|
+
longer ones.
|
|
109
|
+
|
|
110
|
+
`source_dicts` (the live {source_name: parse dict} mapping) enables the
|
|
111
|
+
+/× buttons: + stamps the param into a source that doesn't set it (seeded
|
|
112
|
+
from the view's live resolved value), × pops it from one that does. Both
|
|
113
|
+
are PLAIN dict mutations — the bubbling wrapper marks the owning host
|
|
114
|
+
dirty and its normal chain_out/save path persists the change. Only
|
|
115
|
+
`writable_sources` (real parse dicts, not the absent-source placeholders)
|
|
116
|
+
get the buttons."""
|
|
117
|
+
from meltygui.core.conversion.cache_tree import UNSET_VALUE
|
|
118
|
+
from meltygui.core.styling.fonts import Font
|
|
119
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
120
|
+
from meltygui.view.control_view import button
|
|
121
|
+
from meltygui.view.dropdown_view import draw_dropdown
|
|
122
|
+
from meltygui.view.text_view import draw_text
|
|
123
|
+
from meltygui.core.rendering.render_dispatch import _MATRIX_FRAMEWORK_PARAMS
|
|
124
|
+
from meltygui.model.search_model import _fuzzy_key_match
|
|
125
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
126
|
+
|
|
127
|
+
changed = False
|
|
128
|
+
tints = source_tints or {}
|
|
129
|
+
locations = source_locations or {}
|
|
130
|
+
plus_icon = "\uf067" # FA plus -- explicit escape, see jump_to.py
|
|
131
|
+
times_icon = "" # FA times -- explicit escape, see jump_to.py
|
|
132
|
+
folder_icon = "\uf07b" # FA folder -- explicit escape, see jump_to.py
|
|
133
|
+
|
|
134
|
+
# The dropdown's param list: the view function's own signature params
|
|
135
|
+
# first (plus the MATRIX_DEFAULT_PRIORITY pins, which are listed even
|
|
136
|
+
# when no source row exists for them - their screen just shows every
|
|
137
|
+
# source as not set / +), then the @render_func machinery kwargs.
|
|
138
|
+
# Injected/underscored params are never user inputs, so no screen.
|
|
139
|
+
prio_set = set(priority_params or ())
|
|
140
|
+
params = [p for p in (priority_params or ()) if not p.startswith('_')]
|
|
141
|
+
params += [p for p in input_value
|
|
142
|
+
if p not in prio_set and p not in _MATRIX_FRAMEWORK_PARAMS
|
|
143
|
+
and not p.startswith('_')]
|
|
144
|
+
|
|
145
|
+
search_q = str(search_text or "").strip().lower()
|
|
146
|
+
if search_q:
|
|
147
|
+
params = [p for p in params if _fuzzy_key_match(search_q, p.lower())]
|
|
148
|
+
if not params:
|
|
149
|
+
imgui.text_colored(f"no parameters match '{search_q}'", 1, 1, 1, 0.3)
|
|
150
|
+
return False, input_value
|
|
151
|
+
|
|
152
|
+
selected = getattr(draw_state, "_selected_param", None)
|
|
153
|
+
if selected not in params:
|
|
154
|
+
# Fresh screen (or stale selection) defaults to tint - the param this
|
|
155
|
+
# menu is reached for most - falling back to the first param if a
|
|
156
|
+
# search filter has excluded it.
|
|
157
|
+
selected = "tint" if "tint" in params else params[0]
|
|
158
|
+
draw_state._selected_param = selected
|
|
159
|
+
|
|
160
|
+
draw_text(f"def {view_draw_state._view_func.__name__}",
|
|
161
|
+
is_tree=False, editable=False, width=draw_state.content_width - 14,
|
|
162
|
+
font=Font.JETBRAINS_MONO_30)
|
|
163
|
+
imgui.dummy(0, 4)
|
|
164
|
+
|
|
165
|
+
# STABLE identity: the dropdown's name must not change with the selection -
|
|
166
|
+
# its popover is a latching child window, and a name change would orphan
|
|
167
|
+
# the existing popover. Sync the label as an ordinary input.
|
|
168
|
+
dd_res = draw_dropdown(
|
|
169
|
+
selected, collection={p: p for p in params},
|
|
170
|
+
name=f"param_pick##{unique}",
|
|
171
|
+
width=min(280, max(120, draw_state.content_width - 24)),
|
|
172
|
+
show_header=False, return_extras=True, display_label=selected)
|
|
173
|
+
picked_changed, picked = dd_res[0], dd_res[1]
|
|
174
|
+
if picked_changed and picked in params:
|
|
175
|
+
draw_state._selected_param = picked
|
|
176
|
+
selected = picked
|
|
177
|
+
draw_state.invalidate()
|
|
178
|
+
imgui.same_line()
|
|
179
|
+
origin = "renderer" if selected == "view_func" else ("signature" if selected in prio_set else "core_render.py")
|
|
180
|
+
imgui.text_colored(origin, 1, 1, 1, 0.25)
|
|
181
|
+
imgui.dummy(0, 6)
|
|
182
|
+
|
|
183
|
+
row = input_value.get(selected)
|
|
184
|
+
row = row if isinstance(row, dict) else {}
|
|
185
|
+
# Every registered source gets a row, set or not; sources present only in
|
|
186
|
+
# the row (unmatched leftovers) append after the canonical order.
|
|
187
|
+
order = [s for s in (source_order or ())]
|
|
188
|
+
order += [s for s in row if s not in order]
|
|
189
|
+
|
|
190
|
+
# Uniform label-button width across every row, so values align into a
|
|
191
|
+
# column no matter how long each source's name is.
|
|
192
|
+
btn_w = max((imgui.calc_text_size(f"{folder_icon} {sn}")[0] for sn in order),
|
|
193
|
+
default=0.0) + 15
|
|
194
|
+
|
|
195
|
+
def _stamp_value(param):
|
|
196
|
+
"""Seed for a + click: the view's LIVE resolved value for the param
|
|
197
|
+
(its stamped kwargs, then the draw_state mirror), falling back to the
|
|
198
|
+
first set source's cell — adding a source changes nothing visually
|
|
199
|
+
until the user edits the new value. Deep-copied so the new source
|
|
200
|
+
never aliases another source's parse node; a copied dict's foreign
|
|
201
|
+
__cst__ would mis-anchor the save, so it's stripped."""
|
|
202
|
+
import copy as _copy
|
|
203
|
+
v = (getattr(view_draw_state, '_kwargs', None) or {}).get(param, UNSET_VALUE)
|
|
204
|
+
if v is UNSET_VALUE:
|
|
205
|
+
try:
|
|
206
|
+
v = getattr(view_draw_state, param, None)
|
|
207
|
+
except Exception:
|
|
208
|
+
v = None
|
|
209
|
+
if v is None:
|
|
210
|
+
v = next((row[sn] for sn in order if sn in row), None)
|
|
211
|
+
try:
|
|
212
|
+
v = _copy.deepcopy(v)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
if isinstance(v, dict):
|
|
216
|
+
v.pop('__cst__', None)
|
|
217
|
+
v.pop('__origin__', None)
|
|
218
|
+
return v
|
|
219
|
+
|
|
220
|
+
for sname in order:
|
|
221
|
+
tint = tints.get(sname)
|
|
222
|
+
is_set = sname in row
|
|
223
|
+
src = (source_dicts or {}).get(sname)
|
|
224
|
+
can_write = isinstance(src, dict) and sname in (writable_sources or ())
|
|
225
|
+
|
|
226
|
+
# The source KIND caption (signature / caller / mode / class default /
|
|
227
|
+
# decoration) sits on its own line above the row; the tinted button
|
|
228
|
+
# below displays the concrete name (def draw_voxels / Mode.WINDOW /
|
|
229
|
+
# @defaults(...)) and IS the jump button - clicking opens the source's
|
|
230
|
+
# file in the IDE; the value sits on the same line with
|
|
231
|
+
# show_name=False - so one element does both labeling and navigation.
|
|
232
|
+
kind = (source_kinds or {}).get(sname)
|
|
233
|
+
if selected == "view_func" and kind == "signature" and not is_set:
|
|
234
|
+
can_write = False # Adding a new parameter does not select a renderer.
|
|
235
|
+
if kind:
|
|
236
|
+
imgui.text_colored(kind, 1, 1, 1, 0.5)
|
|
237
|
+
tint_kwargs = {"alpha": 0.0, "tint": tint} if tint else {}
|
|
238
|
+
clicked = button(f"{folder_icon} {sname}", width=btn_w, height=22, shadow=False,
|
|
239
|
+
text_saturation=0.9, use_cache=True, text_align="left",
|
|
240
|
+
text_value=0.819,
|
|
241
|
+
name=f"jump_{sname}##{selected}_{unique}", show_button_bg=True,
|
|
242
|
+
**tint_kwargs)[0]
|
|
243
|
+
loc = locations.get(sname)
|
|
244
|
+
if clicked and loc:
|
|
245
|
+
from meltygui.utils.jump_to_code import open_in_intellij
|
|
246
|
+
threading.Thread(target=open_in_intellij, args=(str(loc[0]),),
|
|
247
|
+
kwargs={"line_number": loc[1]},
|
|
248
|
+
daemon=True).start()
|
|
249
|
+
imgui.same_line()
|
|
250
|
+
|
|
251
|
+
if is_set:
|
|
252
|
+
# × removes the param from this source's dict. A plain dict
|
|
253
|
+
# mutation: the bubbling invalidate notifies the owning object,
|
|
254
|
+
# which goes dirty and persists via its own chain_out/save.
|
|
255
|
+
if isinstance(src, dict) and not (selected == "view_func" and getattr(src, "direct", False)):
|
|
256
|
+
if button(times_icon, width=30, height=22, shadow=True, use_cache=True,
|
|
257
|
+
text_value=1.0, name=f"{sname}_delete##{selected}_{unique}",
|
|
258
|
+
show_button_bg=True, **tint_kwargs)[0]:
|
|
259
|
+
if selected == "view_func" and view_draw_state is not None:
|
|
260
|
+
from meltygui.core.rendering.parameter_core import clear_anywhere
|
|
261
|
+
clear_anywhere(selected, view_draw_state, source=sname)
|
|
262
|
+
else:
|
|
263
|
+
src.pop(selected, None)
|
|
264
|
+
row.pop(sname, None)
|
|
265
|
+
draw_state.invalidate()
|
|
266
|
+
request_render()
|
|
267
|
+
imgui.dummy(0, 3)
|
|
268
|
+
continue
|
|
269
|
+
imgui.same_line()
|
|
270
|
+
|
|
271
|
+
# key routes the cell by ATTRIBUTE name (a tint value gets the
|
|
272
|
+
# swatch/picker, not draw_tuple); the SOURCE stays in the
|
|
273
|
+
# identity via name while the button above displays it.
|
|
274
|
+
cell_view = draw_view_func_selector if selected == "view_func" else draw_any
|
|
275
|
+
ch, nv = cell_view(row[sname], name=f"{sname}##{selected}_{unique}",
|
|
276
|
+
key=selected,
|
|
277
|
+
tint=tint,
|
|
278
|
+
show_name=False, wrap=True,
|
|
279
|
+
bg_offset=2, z_offset=0, disable_scroll=True, width=167)
|
|
280
|
+
if ch:
|
|
281
|
+
if selected == "view_func" and view_draw_state is not None:
|
|
282
|
+
from meltygui.core.rendering.parameter_core import set_anywhere
|
|
283
|
+
set_anywhere(selected, nv, view_draw_state, source=sname)
|
|
284
|
+
else:
|
|
285
|
+
row[sname] = nv
|
|
286
|
+
changed = True
|
|
287
|
+
elif can_write:
|
|
288
|
+
# + stamps the param on this source - same plain-dict write the
|
|
289
|
+
# cell editors use, so the same host-dirty/save machinery runs.
|
|
290
|
+
if button(plus_icon, width=31, height=22, shadow=True, use_cache=True,
|
|
291
|
+
text_value=1.1, name=f"add_{sname}##{selected}_{unique}",
|
|
292
|
+
show_button_bg=True, **tint_kwargs)[0]:
|
|
293
|
+
if selected == "view_func" and view_draw_state is not None:
|
|
294
|
+
from meltygui.core.rendering.parameter_core import set_anywhere
|
|
295
|
+
from meltygui.core.rendering.parameter_core import anywhere_value
|
|
296
|
+
set_anywhere(selected, anywhere_value(selected, view_draw_state),
|
|
297
|
+
view_draw_state, source=sname)
|
|
298
|
+
else:
|
|
299
|
+
src[selected] = _stamp_value(selected)
|
|
300
|
+
row[sname] = src.get(selected)
|
|
301
|
+
draw_state.invalidate()
|
|
302
|
+
request_render()
|
|
303
|
+
imgui.same_line()
|
|
304
|
+
imgui.text_colored("not set", 1, 1, 1, 0.5)
|
|
305
|
+
else:
|
|
306
|
+
imgui.text_colored("not set", 1, 1, 1, 0.5)
|
|
307
|
+
|
|
308
|
+
imgui.dummy(0, 3)
|
|
309
|
+
|
|
310
|
+
return changed, input_value
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def draw_lens(lens, draw_state):
|
|
314
|
+
"""Render a single Lens against draw_state: resolve its root, then either
|
|
315
|
+
focus the live leaf in place (in-place kinds) or run its generated
|
|
316
|
+
parse→focus→save chain (code kinds). Returns (changed, _)."""
|
|
317
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
318
|
+
|
|
319
|
+
from meltygui.code.chain_converters import focus
|
|
320
|
+
root = lens.root(draw_state)
|
|
321
|
+
if root is None:
|
|
322
|
+
imgui.text_colored(f"{lens.kind or lens.label}: n/a here", 0.5, 0.5, 0.5)
|
|
323
|
+
return False, root
|
|
324
|
+
if lens.chain is None:
|
|
325
|
+
return focus(root, path=lens.path, default=lens.default, kind=lens.kind, name=lens.label + lens.name)
|
|
326
|
+
return draw_any(root, chain=lens.chain(root), name=lens.label)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@window
|
|
330
|
+
@render_func()
|
|
331
|
+
def context_menu_settings(input_value, draw_state):
|
|
332
|
+
from meltygui.code.new_converters import code_file_io
|
|
333
|
+
|
|
334
|
+
code_file_io(draw_context_menu, mode=Modes.NEW_CODE)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@render_func(use_cache=False, show_bg=False, show_header=False, show_name=False,
|
|
338
|
+
selectable=False, is_default_for=_InfoRow)
|
|
339
|
+
def draw_info_param(input_value, **kwargs):
|
|
340
|
+
"""One info-tab row: a source dropdown for LOOKING at the different input
|
|
341
|
+
sources, plus the value stored AT the selected source — editable when
|
|
342
|
+
that source is writable, an inline + when it doesn't set the param there
|
|
343
|
+
yet, read-only text otherwise. Shared per-render state (source registry,
|
|
344
|
+
active map, dropdown option cache) arrives via _InfoRow.ctx; selection
|
|
345
|
+
lives on the TAB's draw_state (misc) — pure view state."""
|
|
346
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
347
|
+
from meltygui.view.control_view import draw_button
|
|
348
|
+
from meltygui.view.control_view import text
|
|
349
|
+
from meltygui.view.dropdown_view import draw_dropdown
|
|
350
|
+
from meltygui.core.rendering.parameter_core import set_anywhere
|
|
351
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
352
|
+
|
|
353
|
+
from meltygui.core.rendering.parameter_core import _ABOVE_DRAW_STATE
|
|
354
|
+
row = input_value
|
|
355
|
+
ctx = row.ctx
|
|
356
|
+
if ctx is None or row.group is None:
|
|
357
|
+
# Cold hit before the tab stamped this row's context (shouldn't
|
|
358
|
+
# happen - rows only render from inside the tab body).
|
|
359
|
+
return False, input_value
|
|
360
|
+
param = row.param
|
|
361
|
+
target = ctx.target
|
|
362
|
+
# Key match/current flags come from draw_collection (it matched our name);
|
|
363
|
+
# forwarded to the value widget so the header carries the match, same as
|
|
364
|
+
# the non-collection rows did.
|
|
365
|
+
_search_kw = {"search_match": kwargs.get("search_match", False),
|
|
366
|
+
"search_current": kwargs.get("search_current", False)}
|
|
367
|
+
|
|
368
|
+
if ctx.parses_ready:
|
|
369
|
+
# The ACTIVE source: the highest-priority setter (precomputed in
|
|
370
|
+
# ctx.active_map, one registry pass) - unless a diverged auto_param
|
|
371
|
+
# outranks it at runtime (the ds replaces every setter not in
|
|
372
|
+
# _ABOVE_DRAW_STATE; the ds row only registers whitelisted
|
|
373
|
+
# attrs, so detect the divergence directly. A bare `param in
|
|
374
|
+
# target.__dict__` would be wrong: DrawState.__init__ sets its
|
|
375
|
+
# own param on every param).
|
|
376
|
+
setting = ctx.active_map.get(param)
|
|
377
|
+
ds_has = param in (getattr(target, "auto_params", None) or {})
|
|
378
|
+
if ds_has and (setting is None
|
|
379
|
+
or ctx.prio[setting][0] not in _ABOVE_DRAW_STATE):
|
|
380
|
+
active = "draw_state"
|
|
381
|
+
else:
|
|
382
|
+
active = setting
|
|
383
|
+
ctx.active_cache[param] = active
|
|
384
|
+
known = True
|
|
385
|
+
else:
|
|
386
|
+
# ACTIVE-SOURCE CACHE DISABLED (perf A/B): never serve cached
|
|
387
|
+
# picks - loading rows draw no dropdown until sources are live.
|
|
388
|
+
# Re-enable by restoring: known = param in ctx.active_cache;
|
|
389
|
+
# active = ctx.active_cache.get(param)
|
|
390
|
+
known = False
|
|
391
|
+
active = None
|
|
392
|
+
|
|
393
|
+
if known:
|
|
394
|
+
# Dropdown of ALL sources in SourcePriority order, active one
|
|
395
|
+
# tinted + row-ted - memoized per current active source
|
|
396
|
+
# (ctx.options_for), not rebuilt per param.
|
|
397
|
+
options, _row_tints = ctx.options_for(active)
|
|
398
|
+
|
|
399
|
+
# Selection is per-tab view state; default = the active source.
|
|
400
|
+
sel_key = f"src_sel::{param}"
|
|
401
|
+
sel = ctx.tab_ds.misc.get(sel_key)
|
|
402
|
+
if sel not in options:
|
|
403
|
+
if active is not None:
|
|
404
|
+
sel = active
|
|
405
|
+
elif ctx.parses_ready:
|
|
406
|
+
sel = ctx.default_source_once()
|
|
407
|
+
else:
|
|
408
|
+
sel = "draw_state"
|
|
409
|
+
|
|
410
|
+
# Subtle trigger: no button bg/shadow, short, narrow - it's a
|
|
411
|
+
# provenance label with a dropdown, not a primary action.
|
|
412
|
+
# Per-row trash INSIDE the dropdown row clears this param at THAT
|
|
413
|
+
# source without closing the list, so several sources can be
|
|
414
|
+
# cleared in one operation. Only rows that actually SET the param
|
|
415
|
+
# get one (ctx.setters_map, plus the ds when an auto_param diverged);
|
|
416
|
+
# codec rows are excluded - clear_anywhere can't reverse the
|
|
417
|
+
# codec's per-file/render_kwargs fanout yet.
|
|
418
|
+
def _clear_at(src, _p=param):
|
|
419
|
+
from meltygui.core.rendering.parameter_core import clear_anywhere
|
|
420
|
+
if clear_anywhere(_p, target, str(src)) is not None:
|
|
421
|
+
target.invalidate()
|
|
422
|
+
ctx.tab_ds.invalidate()
|
|
423
|
+
|
|
424
|
+
_row_actions = {s: _clear_at for s in ctx.setters_map.get(param, ())
|
|
425
|
+
if ctx.srcs["kinds"].get(s) != "codec"}
|
|
426
|
+
if param in (getattr(target, "auto_params", None) or {}):
|
|
427
|
+
_row_actions["draw_state"] = _clear_at
|
|
428
|
+
pick_changed, new_pick = draw_dropdown(
|
|
429
|
+
options.get(sel, sel), collection=options, width=181, z_offset=0,
|
|
430
|
+
shadow=False, show_button_bg=False, trigger_height=22, show_bg=False,
|
|
431
|
+
text_pad=3, row_tints=_row_tints, row_actions=_row_actions,
|
|
432
|
+
text_toward_bg=ctx.text_toward_bg,
|
|
433
|
+
name=f"src_{param}_dd", show_header=False)
|
|
434
|
+
if pick_changed and new_pick:
|
|
435
|
+
sel = str(new_pick)
|
|
436
|
+
ctx.tab_ds.misc[sel_key] = sel
|
|
437
|
+
else:
|
|
438
|
+
sel = None
|
|
439
|
+
imgui.dummy(181, 22) # hold the dropdown slot so no reflow when it appears
|
|
440
|
+
|
|
441
|
+
imgui.same_line()
|
|
442
|
+
|
|
443
|
+
any_changed = False
|
|
444
|
+
if not ctx.parses_ready:
|
|
445
|
+
# Sources still loading: stored-at-source reads would hit
|
|
446
|
+
# placeholders - bind the widget to the RESOLVED value and route
|
|
447
|
+
# edits through the automatic pick until the sources are real.
|
|
448
|
+
item_return = draw_any(row.group.get(param), name=param,
|
|
449
|
+
show_bg=False, show_header=True, **_search_kw)
|
|
450
|
+
item_changed, out_val = item_return[0], item_return[1]
|
|
451
|
+
if item_changed:
|
|
452
|
+
row.group[param] = out_val
|
|
453
|
+
any_changed = True
|
|
454
|
+
else:
|
|
455
|
+
# The value AT the selected source (not the resolved value) - that's
|
|
456
|
+
# what looking at a source means. draw_state reads the direct attr.
|
|
457
|
+
sdict = ctx.srcs["sources"].get(sel)
|
|
458
|
+
stored = sdict.get(param) if isinstance(sdict, dict) else None
|
|
459
|
+
if sel == "draw_state" and stored is None:
|
|
460
|
+
stored = (getattr(target, "auto_params", None) or {}).get(
|
|
461
|
+
param, target.__dict__.get(param))
|
|
462
|
+
sel_writable = sel in ctx.writable or sel == "draw_state"
|
|
463
|
+
|
|
464
|
+
# In-flight display cache (_sa_pending - the same one anywhere_value
|
|
465
|
+
# serves): a slow-source write, and EVERY write while a drag is underway
|
|
466
|
+
# (deferred), hasn't reached the source dict yet - a raw stored read
|
|
467
|
+
# snaps the slider back to the stale value next frame ("stuck").
|
|
468
|
+
# Serve the pending UI value while the trip is in flight, but only
|
|
469
|
+
# when this row's selected source is the one the write targeted.
|
|
470
|
+
# The tab's proxy refresh runs anywhere_value per param, which
|
|
471
|
+
# retires it once the stored value moves off its at-set baseline.
|
|
472
|
+
_pending = getattr(target, "_sa_pending", None)
|
|
473
|
+
if _pending and param in _pending:
|
|
474
|
+
_lastsrc = getattr(target, "_sa_last_source", None) or {}
|
|
475
|
+
if _lastsrc.get(param, sel) == sel:
|
|
476
|
+
stored = _pending[param][0]
|
|
477
|
+
|
|
478
|
+
if stored is None:
|
|
479
|
+
if sel_writable:
|
|
480
|
+
# Selected source doesn't set the param yet: + stamps a value
|
|
481
|
+
# into it (creating the entry); next frame the widget takes
|
|
482
|
+
# over. draw_button is the most-compatible in-line button
|
|
483
|
+
# (draw_float's shape) - same header chrome as widget rows.
|
|
484
|
+
clicked, _ = draw_button("+", name=f"+##add_{param}",
|
|
485
|
+
label="+", display_name=param,
|
|
486
|
+
show_name=True, show_bg=False,
|
|
487
|
+
wrap=True, min_width=24, **_search_kw)
|
|
488
|
+
if clicked:
|
|
489
|
+
# Resolved value when there is one; a None (header params
|
|
490
|
+
# nothing sets) stamps the DECLARED signature default -
|
|
491
|
+
# stamping None would create an entry that still reads
|
|
492
|
+
# as None ("the + does nothing" feel).
|
|
493
|
+
stamp = row.group.get(param)
|
|
494
|
+
if stamp is None:
|
|
495
|
+
from meltygui.core.rendering.parameter_core import signature_default_for
|
|
496
|
+
stamp = signature_default_for(param, target)
|
|
497
|
+
set_anywhere(param, stamp, target, allow_any=True,
|
|
498
|
+
ds_fallback=True, source=sel)
|
|
499
|
+
any_changed = True
|
|
500
|
+
else:
|
|
501
|
+
text(f"{param}: not set here", name=f"ro_{param}",
|
|
502
|
+
editable=False, **_search_kw)
|
|
503
|
+
elif sel_writable:
|
|
504
|
+
item_return = draw_any(stored, name=param,
|
|
505
|
+
show_bg=False, show_header=True, **_search_kw)
|
|
506
|
+
item_changed, out_val = item_return[0], item_return[1]
|
|
507
|
+
if item_changed:
|
|
508
|
+
set_anywhere(param, out_val, target, allow_any=True,
|
|
509
|
+
ds_fallback=True, source=sel)
|
|
510
|
+
any_changed = True
|
|
511
|
+
else:
|
|
512
|
+
text(f"{param}: {stored}", name=f"ro_{param}", editable=False,
|
|
513
|
+
**_search_kw)
|
|
514
|
+
|
|
515
|
+
if any_changed:
|
|
516
|
+
target.invalidate()
|
|
517
|
+
# The rows themselves live under the (cached) tab: re-render so the
|
|
518
|
+
# active tint and stored values reflect the write this frame
|
|
519
|
+
# (parse-dict writes are synchronous; the hosts' notify triggers the
|
|
520
|
+
# later save/hotswap).
|
|
521
|
+
ctx.tab_ds.invalidate()
|
|
522
|
+
request_render()
|
|
523
|
+
return False, input_value
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@render_func(use_cache=True, show_bg=False, show_header=False, disable_scroll=False,
|
|
527
|
+
searchable=True, show_name=False, selectable=False)
|
|
528
|
+
@window
|
|
529
|
+
def draw_info_tab(input_value, search_text='', draw_state=None, unique=None, **kwargs):
|
|
530
|
+
"""One row per view param: a source dropdown for LOOKING at the different
|
|
531
|
+
input sources, plus the value stored at the selected source — editable
|
|
532
|
+
when that source is writable, an inline + when it doesn't set the param
|
|
533
|
+
there yet. The dropdown defaults to the source actively driving the
|
|
534
|
+
param (yellow row/trigger); switching it never deletes or moves
|
|
535
|
+
anything, it just changes which source you're viewing/editing.
|
|
536
|
+
Selection lives on THIS tab's draw_state (misc) — pure view state.
|
|
537
|
+
|
|
538
|
+
The source machinery is debug-gated: by default the tab renders just the
|
|
539
|
+
grouped param values (cheap — no registry parses) and the Debug button
|
|
540
|
+
switches to the dropdown rows. The header group starts collapsed in
|
|
541
|
+
both modes."""
|
|
542
|
+
from meltygui.state.inspection_state import _InfoRow
|
|
543
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
544
|
+
from meltygui.view.collection_view import draw_collection
|
|
545
|
+
from meltygui.view.control_view import draw_button
|
|
546
|
+
from meltygui.core.rendering.parameter_core import _source_priority
|
|
547
|
+
from meltygui.core.rendering.parameter_core import _sources_for
|
|
548
|
+
from meltygui.core.rendering.parameter_core import default_write_source
|
|
549
|
+
from meltygui.core.rendering.render_dispatch import _INFO_GROUP_OVERRIDES
|
|
550
|
+
from meltygui.core.rendering.render_dispatch import _SourceItem
|
|
551
|
+
|
|
552
|
+
if input_value is None:
|
|
553
|
+
return False, None
|
|
554
|
+
from meltygui.core.rendering.parameter_core import _unset_value
|
|
555
|
+
target = input_value
|
|
556
|
+
# locate_all_params: the view's own params PLUS the header's
|
|
557
|
+
# (with_header function inputs - icon, show_name, name_color, etc),
|
|
558
|
+
# deduped, view params first. Same read/write semantics.
|
|
559
|
+
proxy = target.locate_all_params
|
|
560
|
+
|
|
561
|
+
# ── Search - the rows render through draw_collection, so it owns key
|
|
562
|
+
# matching: count claims (its _search_matcher over the row keys = the
|
|
563
|
+
# param names), the match/current glow kwargs, and scroll-to-match.
|
|
564
|
+
# Just resolve what to FORWARD: a term/SearchTerm passed in search_text
|
|
565
|
+
# (the menu's search box / an ancestor session), else this tab's OWN
|
|
566
|
+
# find-bar session - the search_text kwarg is NEVER injected from an
|
|
567
|
+
# ancestor session, so a self-hosted Ctrl+F never arrives through it.
|
|
568
|
+
draw_state._search_matcher = None # pre-collection matcher (stale after hotswap)
|
|
569
|
+
_term = search_text or (draw_state.search_text if draw_state.search_active else "")
|
|
570
|
+
if isinstance(_term, SearchTerm):
|
|
571
|
+
child_search = _term
|
|
572
|
+
elif _term and draw_state.search_active and draw_state._search_session is not None:
|
|
573
|
+
# The session (a SearchTerm) carries term + current/scroll_to state.
|
|
574
|
+
child_search = draw_state._search_session
|
|
575
|
+
else:
|
|
576
|
+
child_search = ""
|
|
577
|
+
|
|
578
|
+
# ── Debug gate - the source registry is EXPENSIVE (_sources_for spawns
|
|
579
|
+
# render-func/class/call-site parses, and the codeCM guard below
|
|
580
|
+
# pulses re-renders until they land). Skip ALL of it until asked: by
|
|
581
|
+
# default the tab is just the grouped values (edits route through
|
|
582
|
+
# ParamProxy.__setitem__ → set_anywhere on the driving source); the
|
|
583
|
+
# Debug button fills in the per-param source dropdowns. The pick is
|
|
584
|
+
# per-tab view state, same slot as src_sel.
|
|
585
|
+
debug = bool(draw_state.misc.get("info_sources"))
|
|
586
|
+
clicked, _ = draw_button("Debug", name="dbg_btn", show_name=False,
|
|
587
|
+
label="Hide sources" if debug else "Debug",
|
|
588
|
+
min_width=110)
|
|
589
|
+
if clicked:
|
|
590
|
+
debug = not debug
|
|
591
|
+
draw_state.misc["info_sources"] = debug
|
|
592
|
+
draw_state.invalidate()
|
|
593
|
+
request_render()
|
|
594
|
+
|
|
595
|
+
if not debug:
|
|
596
|
+
# Same grouped shape as the debug mirror, but with the live
|
|
597
|
+
# ParamProxy groups themselves. A fresh outer dict, so the proxy
|
|
598
|
+
# never carries the __overrides__ entry (GroupedParamProxy.refresh
|
|
599
|
+
# would break on a non-proxy value).
|
|
600
|
+
outer = {}
|
|
601
|
+
for _gkey in ("params", "header"):
|
|
602
|
+
_group = proxy.get(_gkey)
|
|
603
|
+
if _group:
|
|
604
|
+
outer[_gkey] = _group
|
|
605
|
+
outer["__overrides__"] = _INFO_GROUP_OVERRIDES
|
|
606
|
+
draw_collection(outer, name="rows", use_cache=False, show_bg=False,
|
|
607
|
+
show_header=False, shadow=False, selectable=False,
|
|
608
|
+
item_spacing_y=2, child_kwargs={"show_system": True,
|
|
609
|
+
"initial":{"expanded":False}
|
|
610
|
+
},
|
|
611
|
+
search_text=child_search)
|
|
612
|
+
return False, input_value
|
|
613
|
+
|
|
614
|
+
srcs = _sources_for(target)
|
|
615
|
+
writable = set(srcs["writable"])
|
|
616
|
+
|
|
617
|
+
# ONE pass over the registry per render - the active source is simply
|
|
618
|
+
# the highest-priority writable source with a SET value. The old shape
|
|
619
|
+
# re-walked every source dict per PARAM (_setting_source per row, plus
|
|
620
|
+
# the default_write_source cue re-counting key overlaps), which is
|
|
621
|
+
# O(params × sources × keys) through lazy bubbling parse wrappers.
|
|
622
|
+
_prio = {s: _source_priority(srcs["kinds"].get(s)) for s in srcs["sources"]}
|
|
623
|
+
_ordered_all = sorted(srcs["sources"], key=_prio.get)
|
|
624
|
+
active_map = {} # param -> highest-priority source
|
|
625
|
+
setters_map = {} # param -> [every source setting it, priority order]
|
|
626
|
+
for _s in _ordered_all:
|
|
627
|
+
if _s not in writable:
|
|
628
|
+
continue
|
|
629
|
+
_sd = srcs["sources"][_s]
|
|
630
|
+
if not isinstance(_sd, dict):
|
|
631
|
+
continue
|
|
632
|
+
for _k, _v in _sd.items():
|
|
633
|
+
if _unset_value(_v):
|
|
634
|
+
continue
|
|
635
|
+
if _k not in active_map:
|
|
636
|
+
active_map[_k] = _s
|
|
637
|
+
setters_map.setdefault(_k, []).append(_s)
|
|
638
|
+
|
|
639
|
+
# The + default for params the source sets (the other-params cue) is
|
|
640
|
+
# param-independent to first order - compute at most once per render,
|
|
641
|
+
# not per row (its overlap counting walks every source dict).
|
|
642
|
+
_cue = []
|
|
643
|
+
|
|
644
|
+
def _default_source_once():
|
|
645
|
+
if not _cue:
|
|
646
|
+
_cue.append(default_write_source("", target, srcs=srcs))
|
|
647
|
+
return _cue[0]
|
|
648
|
+
|
|
649
|
+
# Dropdown styling from Toggles.ContextMenu (live-editable): the active
|
|
650
|
+
# source's yellow, and how far source-row text pulls toward the menu bg.
|
|
651
|
+
_active_tint = tuple(Toggles.ContextMenu.active_source_tint)
|
|
652
|
+
_text_toward_bg = float(Toggles.ContextMenu.source_text_toward_bg)
|
|
653
|
+
|
|
654
|
+
# Dropdown options/row-tints are IDENTICAL for every param sharing the
|
|
655
|
+
# same active source - and a view usually has only one or two distinct
|
|
656
|
+
# actives. Build once per distinct active, not per param (the per-row
|
|
657
|
+
# dict of _SourceItems was N_params × N_sources object churn per render).
|
|
658
|
+
_row_cache = {}
|
|
659
|
+
|
|
660
|
+
def _options_for(active):
|
|
661
|
+
hit = _row_cache.get(active)
|
|
662
|
+
if hit is None:
|
|
663
|
+
options = {s: _SourceItem(s, _active_tint if s == active else None)
|
|
664
|
+
for s in _ordered_all}
|
|
665
|
+
options.setdefault(
|
|
666
|
+
"draw_state",
|
|
667
|
+
_SourceItem("draw_state",
|
|
668
|
+
_active_tint if active == "draw_state" else None))
|
|
669
|
+
row_tints = ({str(active): _active_tint}
|
|
670
|
+
if active is not None else None)
|
|
671
|
+
hit = (options, row_tints)
|
|
672
|
+
_row_cache[active] = hit
|
|
673
|
+
return hit
|
|
674
|
+
|
|
675
|
+
# _sources_for's keep-alive registers the TARGET as the code hosts'
|
|
676
|
+
# consumer; this tab is cached separately, so register it too - a parse
|
|
677
|
+
# landing (an editor's save/hotswap + an external change) then invalidates
|
|
678
|
+
# these rows and the active-source cache tracks the live registry.
|
|
679
|
+
_cm = getattr(target, "_sa_cm_state", None)
|
|
680
|
+
if _cm is not None:
|
|
681
|
+
for _h in (_cm.render_func_dict, _cm.class_dict, _cm.mode_dict,
|
|
682
|
+
*[dh for (_sh, dh) in (_cm.call_site_hosts or [])]):
|
|
683
|
+
if _h is not None:
|
|
684
|
+
_h.notify_on_change(draw_state)
|
|
685
|
+
|
|
686
|
+
# Cold-session guard: while the render-func parse hasn't materialized
|
|
687
|
+
# (signature row still the unwritable placeholder), keep re-rendering.
|
|
688
|
+
# A cached tab stops pulsing the registry and its consumer stamp goes stale,
|
|
689
|
+
# and the parse-landing notify can miss it - the tab then shows "not
|
|
690
|
+
# set here" placeholders forever on a fresh session.
|
|
691
|
+
_sig = next((s for s, k in srcs["kinds"].items() if k == "signature"), None)
|
|
692
|
+
parses_ready = _sig in writable
|
|
693
|
+
if not parses_ready:
|
|
694
|
+
draw_state.invalidate()
|
|
695
|
+
request_render()
|
|
696
|
+
|
|
697
|
+
# Active-source cache on the TARGET ds: parses take a time on a cold
|
|
698
|
+
# open, and provenance shouldn't hide while they load. Ready registry →
|
|
699
|
+
# recompute and refresh the cache; loading → serve the cached pick, and
|
|
700
|
+
# a param with NO cached pick hides the dropdown until the registry is
|
|
701
|
+
# known (the value widget still draws, bound to the resolved value).
|
|
702
|
+
_active_cache = getattr(target, "_sa_active_src", None)
|
|
703
|
+
if _active_cache is None:
|
|
704
|
+
_active_cache = {}
|
|
705
|
+
target._sa_active_src = _active_cache
|
|
706
|
+
|
|
707
|
+
# Everything a row needs to draw its dropdown + stored-value widget,
|
|
708
|
+
# computed ONCE per tab render and shared by every row via _InfoRow.ctx.
|
|
709
|
+
ctx = types.SimpleNamespace(
|
|
710
|
+
target=target, tab_ds=draw_state, srcs=srcs, writable=writable,
|
|
711
|
+
prio=_prio, active_map=active_map, setters_map=setters_map,
|
|
712
|
+
options_for=_options_for, default_source_once=_default_source_once,
|
|
713
|
+
text_toward_bg=_text_toward_bg, parses_ready=parses_ready,
|
|
714
|
+
active_cache=_active_cache)
|
|
715
|
+
|
|
716
|
+
# Mirror the grouped proxy ({'params': {...}, 'header': {...}}) with
|
|
717
|
+
# stable _InfoRow leaves: one row object per param, kept across frames
|
|
718
|
+
# so the child draw_states maintain their identity, rebuilt in proxy order
|
|
719
|
+
# (in place; group dicts' identity is stable too) and pruned as params
|
|
720
|
+
# vanish.
|
|
721
|
+
rows_store = getattr(draw_state, "_info_rows", None)
|
|
722
|
+
if rows_store is None:
|
|
723
|
+
rows_store = {}
|
|
724
|
+
draw_state._info_rows = rows_store
|
|
725
|
+
for _gkey in ("params", "header"):
|
|
726
|
+
_group = proxy.get(_gkey)
|
|
727
|
+
if not _group:
|
|
728
|
+
rows_store.pop(_gkey, None)
|
|
729
|
+
continue
|
|
730
|
+
rows = rows_store.setdefault(_gkey, {})
|
|
731
|
+
_params = list(_group.keys())
|
|
732
|
+
if list(rows.keys()) != _params:
|
|
733
|
+
_prev = dict(rows)
|
|
734
|
+
rows.clear()
|
|
735
|
+
for _p in _params:
|
|
736
|
+
rows[_p] = _prev.get(_p) or _InfoRow(_p)
|
|
737
|
+
for _row in rows.values():
|
|
738
|
+
_row.group = _group
|
|
739
|
+
_row.ctx = ctx
|
|
740
|
+
|
|
741
|
+
# ONE draw_collection over the whole grouped dict - leaf groups render as
|
|
742
|
+
# nested tabs (the point of the grouped shape), leaf rows type-route to
|
|
743
|
+
# draw_info_param and own their writes (always returning changed=False,
|
|
744
|
+
# so nothing gets written back into the mirror). use_cache=False: the
|
|
745
|
+
# tab's own cache is the only gate, as before. Named apart from the
|
|
746
|
+
# edit-mode "rows" so each mode owns its own draw_state subtree.
|
|
747
|
+
rows_store["__overrides__"] = _INFO_GROUP_OVERRIDES # header starts collapsed
|
|
748
|
+
draw_collection(rows_store, name="src_rows", use_cache=False,
|
|
749
|
+
show_bg=False, show_header=False, shadow=False,
|
|
750
|
+
selectable=False, item_spacing_y=2,
|
|
751
|
+
child_kwargs={"show_system": True},
|
|
752
|
+
search_text=child_search)
|
|
753
|
+
return False, input_value
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@render_func(use_cache=True, show_bg=False, show_header=False, show_name=False, selectable=False)
|
|
757
|
+
def draw_config_tab(input_value, **kwargs):
|
|
758
|
+
"""List the inspected view function's configurable parameters and their
|
|
759
|
+
current values (kwarg override, else signature default)."""
|
|
760
|
+
from meltygui.view.control_view import text
|
|
761
|
+
from meltygui.view.text_view import draw_text
|
|
762
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
763
|
+
|
|
764
|
+
view_func = input_value._view_func
|
|
765
|
+
if view_func is None:
|
|
766
|
+
text("No view function")
|
|
767
|
+
return False, input_value
|
|
768
|
+
imgui.dummy(0, 0)
|
|
769
|
+
|
|
770
|
+
# Unwrap the @render_func wrapper to read the original signature.
|
|
771
|
+
raw_func = getattr(view_func, '__wrapped__', view_func)
|
|
772
|
+
sig = inspect.signature(raw_func)
|
|
773
|
+
ds_kwargs = input_value._kwargs or {}
|
|
774
|
+
# Framework-injected params the user doesn't configure.
|
|
775
|
+
skip_params = {"input_value", "draw_state", "args", "o_kwargs",
|
|
776
|
+
"kwargs", "meta", "viewstate", "self"}
|
|
777
|
+
|
|
778
|
+
for param_name, param in sig.parameters.items():
|
|
779
|
+
if param_name in skip_params:
|
|
780
|
+
if param_name in ds_kwargs:
|
|
781
|
+
param_value = ds_kwargs[param_name]
|
|
782
|
+
draw_text(f"{param.__class__.__name__}", name=param_name,
|
|
783
|
+
editable=False, tint=(0.8, 0.8, 0.2))
|
|
784
|
+
continue
|
|
785
|
+
|
|
786
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL,
|
|
787
|
+
inspect.Parameter.VAR_KEYWORD):
|
|
788
|
+
continue
|
|
789
|
+
|
|
790
|
+
# Current value: kwarg override, else the signature default.
|
|
791
|
+
if param_name in ds_kwargs:
|
|
792
|
+
param_value = ds_kwargs[param_name]
|
|
793
|
+
elif param.default is not inspect.Parameter.empty:
|
|
794
|
+
param_value = object()
|
|
795
|
+
else:
|
|
796
|
+
param_value = None
|
|
797
|
+
|
|
798
|
+
if isinstance(param_value, (int, float, str, bool, Enum)):
|
|
799
|
+
draw_text(f"{param_value}", name=param_name, editable=False)
|
|
800
|
+
else:
|
|
801
|
+
draw_any(param_value, name=param_name,
|
|
802
|
+
show_name=True, show_header=True,
|
|
803
|
+
show_add_delete=False, draw=True)
|
|
804
|
+
return False, input_value
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
@render_func(use_cache=True, show_bg=False, live=False, mode=Modes.WINDOW, show_header=False, show_name=False,
|
|
808
|
+
selectable=False)
|
|
809
|
+
def draw_live_tab(input_value, **kwargs):
|
|
810
|
+
"""List the inspected view function's configurable parameters and their
|
|
811
|
+
current values (kwarg override, else signature default)."""
|
|
812
|
+
from meltygui.view.control_view import text
|
|
813
|
+
|
|
814
|
+
imgui.text("Re-renders view frequently, bad for performance but good for debugging")
|
|
815
|
+
|
|
816
|
+
params_to_view = ["unique", ("abs_left", "left_offset"), ("abs_top", "top_offset"), "scroll_offset",
|
|
817
|
+
("abs_top_true", "top_offset_true"), ("width", "height"), ("content_width", "content_height"),
|
|
818
|
+
"layer", "z_offset"]
|
|
819
|
+
for to_view in params_to_view:
|
|
820
|
+
if isinstance(to_view, str):
|
|
821
|
+
value = getattr(input_value, to_view, 'N/A')
|
|
822
|
+
text(f"{to_view}: {value}", name=to_view, editable=False)
|
|
823
|
+
else:
|
|
824
|
+
values = [getattr(input_value, attr, 'N/A') for attr in to_view]
|
|
825
|
+
text(f"{', '.join(to_view)}: {', '.join(str(v) for v in values)}", name=", ".join(to_view), editable=False)
|
|
826
|
+
|
|
827
|
+
if isinstance(input_value._raw_input_value, (dict, list, tuple)):
|
|
828
|
+
text(f"Length: {len(input_value._raw_input_value)}", name="raw_input_length", editable=False)
|
|
829
|
+
|
|
830
|
+
# imgui.text_colored(f"Unique {input_value.unique}", *(0.5, 0.01, 0.6))
|
|
831
|
+
# imgui.dummy(0,2)
|
|
832
|
+
#
|
|
833
|
+
# imgui.text_colored(f"Top, Left {input_value.abs_top}, {input_value.abs_left}", *(0.5, 0.5, 0.0))
|
|
834
|
+
# imgui.dummy(0, 2)
|
|
835
|
+
#
|
|
836
|
+
# #Width, height
|
|
837
|
+
# imgui.text_colored(f"Width, Height {input_value.width}, {input_value.height}", *(0.5, 0.01, 0.6))
|
|
838
|
+
# imgui.dummy(0, 2)
|
|
839
|
+
#
|
|
840
|
+
# imgui.text_colored(f"Unique {input_value.unique}", *(0.5, 0.01, 0.6))
|
|
841
|
+
# imgui.dummy(0, 2)
|
|
842
|
+
|
|
843
|
+
return False, input_value
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def draw_func_tab(input_value, name=None, disable_scroll=True, width=None,
|
|
847
|
+
height=None, select_line=None, select_seq=0, **kwargs):
|
|
848
|
+
"""Editable source of the inspected view function; hotswaps on save.
|
|
849
|
+
Routes through Mode.FILE_TREE — the same cache-backed code_file_io path a
|
|
850
|
+
folder-files leaf uses — so all editors share one code path.
|
|
851
|
+
|
|
852
|
+
A plain function, not a @render_func: the wrapper added a full pass +
|
|
853
|
+
tile layer around a single dispatch (the standing `draw_func_tab` line
|
|
854
|
+
in the frame profiles) and the editor child does its own caching. The
|
|
855
|
+
caller's `name` rides into the child as its `key` so two func tabs
|
|
856
|
+
showing the same function keep distinct draw_states — the wrapper's
|
|
857
|
+
per-tab name used to provide that separation.
|
|
858
|
+
|
|
859
|
+
code_file_io is called DIRECTLY with Mode.FILE_TREE's override kwargs
|
|
860
|
+
(chain idiom) instead of via draw_any(mode=FILE_TREE): the mode pins
|
|
861
|
+
disable_scroll=True and mode kwargs win over call kwargs, so the
|
|
862
|
+
caller's disable_scroll=False could never reach the editor — the old
|
|
863
|
+
wrapper was the scroll container, and removing it killed scrolling
|
|
864
|
+
until this bypass."""
|
|
865
|
+
from meltygui.view.control_view import draw_str
|
|
866
|
+
|
|
867
|
+
view_func = input_value._view_func
|
|
868
|
+
if view_func is not None:
|
|
869
|
+
from meltygui.code.new_converters import code_file_io
|
|
870
|
+
from meltygui.view.code_view import draw_text_from_code_cache
|
|
871
|
+
view_func_name = view_func.__name__ if hasattr(view_func, '__name__') else str(view_func)
|
|
872
|
+
_kw = {}
|
|
873
|
+
if width is not None:
|
|
874
|
+
_kw["width"] = width
|
|
875
|
+
if height is not None:
|
|
876
|
+
_kw["height"] = height
|
|
877
|
+
if name is not None:
|
|
878
|
+
_kw["key"] = name
|
|
879
|
+
if select_line is not None:
|
|
880
|
+
# File-absolute line to auto-select (scope-up nav) — rides through
|
|
881
|
+
# code_file_io's child_kwargs into draw_text_from_code_cache, which
|
|
882
|
+
# consumes it against the span buffer. select_seq (the key-press
|
|
883
|
+
# generation) keys the one-shot so each press re-selects.
|
|
884
|
+
_kw["child_kwargs"] = {"select_line": select_line,
|
|
885
|
+
"select_seq": select_seq}
|
|
886
|
+
code_file_io(view_func, auto_load_edits=True,
|
|
887
|
+
view_func=draw_text_from_code_cache,
|
|
888
|
+
disable_scroll=disable_scroll, show_name=False,
|
|
889
|
+
is_tree=False, name=view_func_name, **_kw)
|
|
890
|
+
else:
|
|
891
|
+
draw_str("No view function specified", name="View Function", editable=False)
|
|
892
|
+
return False, input_value
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
@render_func(use_cache=True, show_bg=False, show_header=False, show_name=False, selectable=False)
|
|
896
|
+
def draw_eval_tab(input_value, draw_state, unique=None, enter_key_down=None,
|
|
897
|
+
menu_draw_state=None, **kwargs):
|
|
898
|
+
"""Arbitrary-code REPL scoped to the inspected view function. This tab is just
|
|
899
|
+
an editor + trigger: it stashes the snippet and a pending flag on the TARGET
|
|
900
|
+
widget's draw_state. The eval itself runs back in that widget's render wrapper
|
|
901
|
+
(core_render), right before it calls the view func -- so the snippet sees the
|
|
902
|
+
view function's real call-time locals. We read the result back off the same
|
|
903
|
+
draw_state."""
|
|
904
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
905
|
+
from meltygui.view.control_view import button
|
|
906
|
+
from meltygui.view.text_view import draw_text
|
|
907
|
+
from meltygui.core.rendering.render_dispatch import eval_input_scope
|
|
908
|
+
import meltygui.core.windowing.window_api as glfw
|
|
909
|
+
|
|
910
|
+
target = input_value # (possibly walked-up) target's draw_state
|
|
911
|
+
eval_view_func = target._view_func
|
|
912
|
+
|
|
913
|
+
# Autocomplete scope. The exact call-time locals are only known once an eval
|
|
914
|
+
# actually fires (core_scoped_eval pulls them then). To give type-aware
|
|
915
|
+
# suggestions BEFORE the first eval, pre-record an initial scope from what
|
|
916
|
+
# the target draw_state already exposes -- input_value/value, draw_state/ds,
|
|
917
|
+
# and every explicit kwarg. core_scoped_eval refines this to exact on first eval.
|
|
918
|
+
from meltygui.core.rendering.func_metadata import FuncsMetadata
|
|
919
|
+
from meltygui.core.rendering.func_metadata import eval_completion_source
|
|
920
|
+
_scope = eval_input_scope(target)
|
|
921
|
+
if isinstance(target._kwargs, dict):
|
|
922
|
+
_scope.update(target._kwargs)
|
|
923
|
+
_scope.update({"input_value": target._raw_input_value, "value": target._raw_input_value,
|
|
924
|
+
"draw_state": target, "ds": target})
|
|
925
|
+
FuncsMetadata.record(eval_view_func, _scope)
|
|
926
|
+
|
|
927
|
+
# The snippet lives on the TARGET's draw_state as a persisted `eval_code`
|
|
928
|
+
# field (excluded from auto-invalidation: typing must not re-render the
|
|
929
|
+
# inspected view), so it reopens with what was last typed for this view.
|
|
930
|
+
code = target.eval_code
|
|
931
|
+
if code is None:
|
|
932
|
+
code = "input_value"
|
|
933
|
+
# Single-line editor: Enter never reaches the editor as a newline (its newline
|
|
934
|
+
# handler is gated on `not single_line`); instead the menu claims the
|
|
935
|
+
# enter-down event via its on_enter_key_down param and uses it to fire the
|
|
936
|
+
# eval below.
|
|
937
|
+
# return_extras gives the code box's draw_state so we can tell when it holds
|
|
938
|
+
# text focus (and thus when Enter should fire the eval -- see below).
|
|
939
|
+
box = draw_text(
|
|
940
|
+
code, name=f"eval_code##{unique}", padding_right=100,
|
|
941
|
+
single_line=True, show_bg=True, show_header=False,
|
|
942
|
+
completion_source=eval_completion_source(eval_view_func),
|
|
943
|
+
return_extras=True, tint=(0.05, 0.15, 0.08))
|
|
944
|
+
code_changed, new_code = box[0], box[1]
|
|
945
|
+
code_ds = box[2] if len(box) > 2 else None
|
|
946
|
+
if code_changed:
|
|
947
|
+
target.eval_code = new_code
|
|
948
|
+
code = new_code
|
|
949
|
+
|
|
950
|
+
def _fire_eval():
|
|
951
|
+
# Stash the snippet + arm the trigger, then force the target to actually
|
|
952
|
+
# re-render (bypassing its cache) so its wrapper runs func() -- and our
|
|
953
|
+
# eval hook -- this/next frame.
|
|
954
|
+
target.eval_code = code
|
|
955
|
+
target._eval_pending = True
|
|
956
|
+
target._eval_request_gen = getattr(target, '_eval_generation', 0) + 1
|
|
957
|
+
target.invalidate()
|
|
958
|
+
target._parent.invalidate_up(max_depth=5)
|
|
959
|
+
request_render()
|
|
960
|
+
|
|
961
|
+
run_clicked = button("Run", height=30, name=f"eval_run##{unique}",
|
|
962
|
+
color=(0.2, 0.7, 0.3), factor=0.8)[0]
|
|
963
|
+
# Enter fires the eval. Listen for it directly (the way draw_text reads keys
|
|
964
|
+
# off the frame queue) instead of relying on the menu to forward it: while the
|
|
965
|
+
# single-line code box holds text focus, Enter never reaches it as a newline,
|
|
966
|
+
# so we claim it here when that box is the focused editor. But when the
|
|
967
|
+
# autocomplete popup is open, Enter ACCEPTS the highlighted suggestion (the box
|
|
968
|
+
# consumes it + splices text),), so we must NOT also fire the eval that press.
|
|
969
|
+
# `code_changed` is the reliable gate: a "run" Enter (popup closed) leaves the
|
|
970
|
+
# single-line buffer untouched, while an accept always rewrites it. (Reading
|
|
971
|
+
# code_ds._popup_open here is too late; draw_text already cleared it on accept.)
|
|
972
|
+
enter_pressed = (code_ds is not None and not code_changed
|
|
973
|
+
and Core.melty.text_focused_ds is code_ds
|
|
974
|
+
and any(k in (glfw.KEY_ENTER, glfw.KEY_KP_ENTER)
|
|
975
|
+
for k, _ in Core.melty.frame_key_events))
|
|
976
|
+
if run_clicked or enter_pressed:
|
|
977
|
+
_fire_eval()
|
|
978
|
+
|
|
979
|
+
# The eval lands in the target's wrapper, a separate render pass. Until the
|
|
980
|
+
# requested generation is served, keep THIS tab (and the owning menu) live so
|
|
981
|
+
# we re-run and re-read the fresh result rather than serving a stale cache.
|
|
982
|
+
if getattr(target, '_eval_generation', 0) < getattr(target, '_eval_request_gen', 0):
|
|
983
|
+
draw_state.invalidate() # this tab's own draw_state
|
|
984
|
+
if menu_draw_state is not None:
|
|
985
|
+
menu_draw_state.invalidate() # the menu so it re-calls this tab
|
|
986
|
+
request_render()
|
|
987
|
+
|
|
988
|
+
eval_result = getattr(target, '_eval_result', None)
|
|
989
|
+
if eval_result:
|
|
990
|
+
# Titled with the snippet that produced it (stamped beside the result
|
|
991
|
+
# by the wrapper's eval hook), so a finished run is visible at a
|
|
992
|
+
# glance; no tree collapser - the result is always open.
|
|
993
|
+
result_title = getattr(target, '_eval_result_code', None) or "eval"
|
|
994
|
+
draw_text(eval_result, name=f"eval_result##{unique}",
|
|
995
|
+
display_name=result_title, is_tree=False,
|
|
996
|
+
show_bg=True, show_header=True,
|
|
997
|
+
show_name=True, editable=False, height=400, min_height=400,
|
|
998
|
+
wrap=True, bg_offset=-100, width=draw_state.content_width,
|
|
999
|
+
tint=(0.0, 0.0, 0.0))
|
|
1000
|
+
return False, input_value
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
@render_func(use_cache=False, show_bg=False, show_header=False, show_name=False, selectable=False, disable_scroll=False,
|
|
1004
|
+
temp=True, searchable=True)
|
|
1005
|
+
def draw_input_tab(input_value, cm_state: ContextMenuState, draw_state, wrap=True, unique=None, class_to_show=None,
|
|
1006
|
+
enter_key_pressed=None, **kwargs):
|
|
1007
|
+
"""The three editable sources behind this view, in dispatch order:
|
|
1008
|
+
|
|
1009
|
+
1. RENDER FUNCTION — the render_func whose body produced the view, edited
|
|
1010
|
+
whole via FunctionCodec. It isn't on the captured stack (the capture runs
|
|
1011
|
+
in the wrapper BEFORE the body executes, so the innermost frame is the
|
|
1012
|
+
filtered wrapper), so it's read from `_view_func`.
|
|
1013
|
+
2. CALLER — the direct `draw_x(...)` call site that invoked this view, edited
|
|
1014
|
+
via CallerCodec (spans just the call expression). `_call_site` is the
|
|
1015
|
+
nearest real caller (filename, lineno), captured once on menu-open with the
|
|
1016
|
+
render-dispatch machinery already filtered out (caller_site, core_render).
|
|
1017
|
+
3. DECORATIONS — the `@...` block on the value's class, edited via
|
|
1018
|
+
DecorationsCodec. `class_to_show` is resolved by draw_context_menu (the
|
|
1019
|
+
value's own class, or the nearest parent with source for a primitive
|
|
1020
|
+
field); only classes carry decorations, so it's skipped otherwise.
|
|
1021
|
+
4. MODE — the ACTIVE mode's entry kwargs in its enum class source
|
|
1022
|
+
(mode.py), edited via ModeCodec. Which member to show comes from the
|
|
1023
|
+
target's _kwargs ('current_mode', stamped by the wrapper when a mode
|
|
1024
|
+
config matched); skipped when no mode drove this view."""
|
|
1025
|
+
from meltygui.code.new_converters import host_code_state
|
|
1026
|
+
from meltygui.code.new_converters import recompile_button
|
|
1027
|
+
from meltygui.code.new_converters import recompile_status
|
|
1028
|
+
from meltygui.code.new_converters import run_recompile
|
|
1029
|
+
from meltygui.core.rendering.parameter_core import _source_priority
|
|
1030
|
+
from meltygui.core.rendering.render_dispatch import apply_param_source_matrix
|
|
1031
|
+
from meltygui.core.rendering.render_dispatch import collect_input_sources
|
|
1032
|
+
from meltygui.core.rendering.render_dispatch import param_source_matrix
|
|
1033
|
+
from meltygui.core.rendering.render_dispatch import signature_param_names
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
srcs = collect_input_sources(input_value, cm_state, class_to_show)
|
|
1037
|
+
sources, source_tints = srcs["sources"], srcs["tints"]
|
|
1038
|
+
source_locations, source_kinds = srcs["locations"], srcs["kinds"]
|
|
1039
|
+
writable_sources = srcs["writable"]
|
|
1040
|
+
|
|
1041
|
+
# ── Recompile (hotswap) - the same Run path code_file_io draws on a file
|
|
1042
|
+
# leaf (folder_files / FILE_TREE). A matrix edit saves SOURCE to disk via
|
|
1043
|
+
# the hosts' chain_out, but the live view function keeps its old defaults
|
|
1044
|
+
# until a hotswap. The button/Run work against the view_host's own
|
|
1045
|
+
# CodeState, the Run compiles the host's live root (the edit already
|
|
1046
|
+
# merged in), not a possibly-stale disk copy. None until the lazy host has
|
|
1047
|
+
# drawn/updated - the button appears a beat after the menu opens.
|
|
1048
|
+
code_state = host_code_state(cm_state.render_func_str)
|
|
1049
|
+
if code_state is not None and code_state.address is not None:
|
|
1050
|
+
clicked = recompile_button(code_state, unique=unique)
|
|
1051
|
+
recompile_status(code_state, draw_state)
|
|
1052
|
+
# Alt+Enter (or the editor's usual Ctrl+Enter) while over the tab -
|
|
1053
|
+
# enter_key_pressed is the menu-subscribed Enter-down InputEvent, same
|
|
1054
|
+
# mechanism as code_file_io's hotkey; modifiers ride on the event.
|
|
1055
|
+
hotkey = bool(enter_key_pressed and (enter_key_pressed.alt or enter_key_pressed.ctrl))
|
|
1056
|
+
run_recompile(input_value._view_func, code_state, draw_state,
|
|
1057
|
+
start=clicked or hotkey, name=f"recompile{unique}")
|
|
1058
|
+
|
|
1059
|
+
# ── Search - the STANDARD searchable path, no special filter box. The tab
|
|
1060
|
+
# is searchable=True, so Ctrl+F on it opens the framework's floating
|
|
1061
|
+
# find bar (core_render's searchable block), which maintains the term on
|
|
1062
|
+
# this draw_state.search_text and invalidates this subtree per keystroke.
|
|
1063
|
+
# That same text hits draw_param_matrix's live param filter below (which
|
|
1064
|
+
# auto-switches the screen to the best match), and the search session on
|
|
1065
|
+
# Melty.search_stack reaches the matrix cells' strings for in-place
|
|
1066
|
+
# highlighting like any other searchable widget.
|
|
1067
|
+
|
|
1068
|
+
from meltygui.core.rendering.parameter_core import anywhere_value
|
|
1069
|
+
anywhere_value("view_func", input_value)
|
|
1070
|
+
if sources:
|
|
1071
|
+
_, matrix = param_source_matrix(sources, func=input_value._view_func,
|
|
1072
|
+
include_unmatched=True)
|
|
1073
|
+
changed, value = draw_param_matrix(matrix, source_tints=source_tints,
|
|
1074
|
+
source_locations=source_locations,
|
|
1075
|
+
# Rows in SourceKind order, highest
|
|
1076
|
+
# (the first candidates) on top;
|
|
1077
|
+
# registration order breaks ties
|
|
1078
|
+
# (sorted is stable).
|
|
1079
|
+
source_order=tuple(sorted(
|
|
1080
|
+
sources,
|
|
1081
|
+
key=lambda n: _source_priority(
|
|
1082
|
+
source_kinds.get(n)))),
|
|
1083
|
+
source_dicts=sources,
|
|
1084
|
+
writable_sources=tuple(writable_sources),
|
|
1085
|
+
source_kinds=source_kinds,
|
|
1086
|
+
view_draw_state=input_value, wrap=True,
|
|
1087
|
+
width=draw_state.content_width - 0,
|
|
1088
|
+
priority_params=tuple(signature_param_names(input_value._view_func)),
|
|
1089
|
+
search_text=str(draw_state.search_text or ""),
|
|
1090
|
+
name=f"{input_value._view_func.__name__} inputs##matrix{unique}",
|
|
1091
|
+
disable_scroll=True)
|
|
1092
|
+
if changed and isinstance(value, dict):
|
|
1093
|
+
# Pure write-back (no UI) - call the bare function, not this wrapper.
|
|
1094
|
+
apply_param_source_matrix.__wrapped__(value, ref=sources, changed=True)
|
|
1095
|
+
|
|
1096
|
+
# Each *_dict RenderHost parses its source on a background worker in its OWN draw
|
|
1097
|
+
# loop; this tab merely READS the materialized value (h.deep....) and draws it. When
|
|
1098
|
+
# a parse lands, the host's after_render() runs the loop but can't reach this
|
|
1099
|
+
# cached subtree - so register this tab's draw_state as a listener and the host
|
|
1100
|
+
# invalidates us when its value changes. Replaces the old "invalidate for the first
|
|
1101
|
+
# 10ms" guess, which expired before the ~400ms chain_in debounce, leaving the
|
|
1102
|
+
# dict blank until a manual mouse-over.
|
|
1103
|
+
caller_dict_hosts = [dh for (_sh, dh) in cm_state.call_site_hosts]
|
|
1104
|
+
for _h in (cm_state.render_func_dict, cm_state.decoration_dict, cm_state.class_dict,
|
|
1105
|
+
*caller_dict_hosts, cm_state.mode_dict):
|
|
1106
|
+
if _h is not None:
|
|
1107
|
+
_h.notify_on_change(draw_state)
|
|
1108
|
+
|
|
1109
|
+
# common = dict(mode=Modes.NEW_CODE, min_width=100, max_height=300, fill_height=False)
|
|
1110
|
+
#
|
|
1111
|
+
# view_func = getattr(input_value, "_view_func", None)
|
|
1112
|
+
# if inspect.isfunction(view_func):
|
|
1113
|
+
# draw_any(view_func, name=f"{view_func.__name__} (self)##self_{unique}", **common)
|
|
1114
|
+
|
|
1115
|
+
# call_site = getattr(input_value, "_call_site", None)
|
|
1116
|
+
# if call_site is not None:
|
|
1117
|
+
# filename, lineno = call_site
|
|
1118
|
+
# draw_any(CallSite(filename, lineno), name=f"caller:{lineno}##caller_{unique}", **common)
|
|
1119
|
+
#
|
|
1120
|
+
# if isinstance(class_to_show, type):
|
|
1121
|
+
# draw_any(Decorations(class_to_show),
|
|
1122
|
+
# name=f"{class_to_show.__name__} decorations##deco_{unique}", **common)
|
|
1123
|
+
|
|
1124
|
+
return False, input_value
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
@render_func(use_cache=True, show_bg=False, show_header=False, disable_scroll=False, show_name=False, selectable=False)
|
|
1128
|
+
def draw_class_tab(input_value, class_to_show=None, class_is_parent=False, class_name='', **kwargs):
|
|
1129
|
+
"""Editable class source. For a primitive field this is the parent object's
|
|
1130
|
+
class (e.g. Lora for a Lora.rank float) -- labelled so the source is clear.
|
|
1131
|
+
Routes through Mode.FILE_TREE — the same cache-backed code_file_io path a
|
|
1132
|
+
folder-files leaf uses — so all editors share one code path."""
|
|
1133
|
+
from meltygui.view.control_view import text
|
|
1134
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
1135
|
+
|
|
1136
|
+
from meltygui.core.rendering.mode import Mode
|
|
1137
|
+
if class_is_parent:
|
|
1138
|
+
text(f"Parent type of {class_name}", name="Source",
|
|
1139
|
+
editable=False, tint=(1.0, 0.64, 0.113))
|
|
1140
|
+
cls_change, new_cls = draw_any(class_to_show, mode=Mode.FILE_TREE,
|
|
1141
|
+
name=class_to_show.__name__)
|
|
1142
|
+
return False, input_value
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
@render_func(use_cache=True, show_bg=False, show_header=False, show_name=False, selectable=False)
|
|
1146
|
+
def draw_mode_tab(input_value, draw_state, current_mode=None, **kwargs):
|
|
1147
|
+
"""Show the current Mode's value string."""
|
|
1148
|
+
from meltygui.view.control_view import text
|
|
1149
|
+
|
|
1150
|
+
if current_mode is not None:
|
|
1151
|
+
mode_change, new_mode = text(str(current_mode.value), width=draw_state.content_width,
|
|
1152
|
+
name=str(current_mode))
|
|
1153
|
+
return False, input_value
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def draw_context_menu_items(draw_state, items, right_click, name, unique):
|
|
1157
|
+
"""The `context_menu={label: callable}` popover of a view: the dropdown's
|
|
1158
|
+
own menu (draw_dd_menu — rows, hover, keys, click-away) opened AT THE
|
|
1159
|
+
POINTER by a right-click instead of under a trigger button. Called by
|
|
1160
|
+
the wrapper (core_render's context-menu block) on every body run of the
|
|
1161
|
+
view, `right_click` = a right-click landed on it this run.
|
|
1162
|
+
|
|
1163
|
+
Open/closed is the dropdowns' slot: the VIEW holds Melty.popover_focused_ds
|
|
1164
|
+
while its menu is open, exactly as a dropdown trigger does, so a click
|
|
1165
|
+
anywhere outside the menu hands the slot on (the wrapper's click-away
|
|
1166
|
+
clear_focus, which also re-runs this view) and the next call draws the
|
|
1167
|
+
menu closed. The menu is a latching window: it is called EVERY run with
|
|
1168
|
+
`closed=` so its window re-registers on each open (a window drawn only
|
|
1169
|
+
while its draw_state is already closed never registers — the reopen
|
|
1170
|
+
that died 09-10).
|
|
1171
|
+
|
|
1172
|
+
A picked row runs its callable here and closes the menu; the Inspect row
|
|
1173
|
+
at the bottom reports INSPECT so the wrapper opens the inspector in the
|
|
1174
|
+
menu's place. Returns INSPECT, the picked label, or None."""
|
|
1175
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1176
|
+
from meltygui.view.dropdown_view import draw_dd_menu
|
|
1177
|
+
from meltygui.core.rendering.render_dispatch import INSPECT
|
|
1178
|
+
from meltygui.model.dropdown_model import _dd_as_tuple
|
|
1179
|
+
from meltygui.core.layout.dropdown_core import _dd_close
|
|
1180
|
+
from meltygui.model.dropdown_model import _dd_label_for_path
|
|
1181
|
+
from meltygui.core.layout.dropdown_core import _dd_update_menu_size
|
|
1182
|
+
from meltygui.core.layout.dropdown_core import _ds_in_subtree
|
|
1183
|
+
import meltygui.core.windowing.window_api as glfw
|
|
1184
|
+
|
|
1185
|
+
from meltygui.state.new_core_model import ContextMenuItemsState
|
|
1186
|
+
# [tint=(0.85, 0.75, 0.05)]
|
|
1187
|
+
inspect_label = f" Inspect"
|
|
1188
|
+
inspect_tint = (0.42, 0.24, 0.06)
|
|
1189
|
+
state_key = "context_menu_items_state"
|
|
1190
|
+
|
|
1191
|
+
# The menu's state lives in the view's misc, where injected states go,
|
|
1192
|
+
# so it persists with the draw_state (DropDownState: cursor/open paths,
|
|
1193
|
+
# the drag-resized menu_size) and carries where the menu opened.
|
|
1194
|
+
state = draw_state.misc.get(state_key)
|
|
1195
|
+
if not isinstance(state, ContextMenuItemsState):
|
|
1196
|
+
state = draw_state.misc[state_key] = ContextMenuItemsState()
|
|
1197
|
+
draw_state.misc_used.add(state_key)
|
|
1198
|
+
|
|
1199
|
+
is_open = Melty.popover_focused_ds is draw_state
|
|
1200
|
+
if right_click:
|
|
1201
|
+
if is_open:
|
|
1202
|
+
Melty.popover_focused_ds = None
|
|
1203
|
+
_dd_close(state)
|
|
1204
|
+
else:
|
|
1205
|
+
Melty.popover_focused_ds = draw_state
|
|
1206
|
+
Melty._popover_open_frame = Melty.frame_count # grace the opening click
|
|
1207
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
1208
|
+
state.open_at = (mouse_x - draw_state.abs_left, mouse_y - draw_state.abs_top)
|
|
1209
|
+
_dd_close(state)
|
|
1210
|
+
state._kbd_mode = False
|
|
1211
|
+
state._last_mouse = None
|
|
1212
|
+
is_open = not is_open
|
|
1213
|
+
draw_state.invalidate()
|
|
1214
|
+
request_render()
|
|
1215
|
+
|
|
1216
|
+
collection = {str(label): action for label, action in items.items()}
|
|
1217
|
+
collection[inspect_label] = INSPECT
|
|
1218
|
+
|
|
1219
|
+
# Popover size: content-fit until the user drag-resizes it - the same
|
|
1220
|
+
# mechanism as draw_dropdown (see its popover-size comment): a forced
|
|
1221
|
+
# size goes on before the window begins, else a fit is stamped after
|
|
1222
|
+
# every open frame and a size that differs is the resize handle's work.
|
|
1223
|
+
menu_ds = state._menu_ds
|
|
1224
|
+
menu_size = state.menu_size
|
|
1225
|
+
if is_open and menu_ds is not None:
|
|
1226
|
+
opening = getattr(Melty, "_popover_open_frame", None) == Melty.frame_count
|
|
1227
|
+
if opening and menu_size is not None:
|
|
1228
|
+
menu_ds.width, menu_ds.height = menu_size
|
|
1229
|
+
state._menu_fit = tuple(menu_size)
|
|
1230
|
+
if menu_ds.width is None or menu_ds.width < 5:
|
|
1231
|
+
menu_ds.width = Toggles.Dropdown.min_width
|
|
1232
|
+
# The menu's top-left sits this far RIGHT of the pointer, so the pointer
|
|
1233
|
+
# rests on the first row rather than on the popover's left edge (the
|
|
1234
|
+
# wrapper's resize handle) — Lukas 09-10.
|
|
1235
|
+
# [tint=(0.939, 0.453, 0.245)]
|
|
1236
|
+
pointer_inset_x = 8
|
|
1237
|
+
# Below the pointer unless that runs past the display edge - then above it.
|
|
1238
|
+
open_x, open_y = state.open_at
|
|
1239
|
+
open_x += pointer_inset_x
|
|
1240
|
+
if menu_ds is not None and menu_ds.height:
|
|
1241
|
+
display_h = imgui.get_io().display_size[1]
|
|
1242
|
+
if draw_state.abs_top + open_y + menu_ds.height > display_h - 10:
|
|
1243
|
+
open_y -= menu_ds.height
|
|
1244
|
+
# The pointer is the anchor through the CURSOR (draw_tuple_fast's picker
|
|
1245
|
+
# does the same), never through window_pos: the wrapper folds a nested
|
|
1246
|
+
# window's window_pos offset into its content measure, so an offset
|
|
1247
|
+
# menu grew by that offset every frame. Restored after — the wrapper
|
|
1248
|
+
# reads the cursor right after this block for the view's own header.
|
|
1249
|
+
# Row labels are Tint.dd_text over the menu tint (its value × 2.16). A view
|
|
1250
|
+
# sitting in a dark-tinted window hands over a dark tint and the labels
|
|
1251
|
+
# came out dim against the popover (Lukas 09-10), so the tint's value is
|
|
1252
|
+
# floored here — raise the floor for brighter labels. The popover bg is
|
|
1253
|
+
# darkened by depth from the same tint and barely moves with it.
|
|
1254
|
+
# [tint=(0.994, 0.872, 0.0)]
|
|
1255
|
+
menu_tint_value_floor = 0.5
|
|
1256
|
+
menu_tint = draw_state.tint
|
|
1257
|
+
if menu_tint is not None:
|
|
1258
|
+
hue, saturation, value = rgb_to_hsv(*menu_tint[:3])
|
|
1259
|
+
if value < menu_tint_value_floor:
|
|
1260
|
+
menu_tint = hsv_to_rgb(hue, saturation, menu_tint_value_floor)
|
|
1261
|
+
cursor = imgui.get_cursor_screen_pos()
|
|
1262
|
+
imgui.set_cursor_screen_pos((draw_state.abs_left + open_x, draw_state.abs_top + open_y))
|
|
1263
|
+
changed, picked, menu_ds = draw_dd_menu(
|
|
1264
|
+
collection, tint=menu_tint,
|
|
1265
|
+
name=f"{name}##context_menu_items_{unique}",
|
|
1266
|
+
closed=not is_open, temp=True, shadow=False, auto_resize=False,
|
|
1267
|
+
window_pos=(0, 0), max_height=Toggles.Dropdown.max_height,
|
|
1268
|
+
parent_window=draw_state, swoosh=False, disable_scroll=False,
|
|
1269
|
+
show_search=False, row_tints={INSPECT: inspect_tint},
|
|
1270
|
+
root_state=state, path_prefix=(), return_extras=True)
|
|
1271
|
+
imgui.set_cursor_screen_pos(cursor)
|
|
1272
|
+
state._menu_ds = menu_ds
|
|
1273
|
+
if not is_open or menu_ds is None:
|
|
1274
|
+
return None
|
|
1275
|
+
|
|
1276
|
+
_dd_update_menu_size(state, menu_ds)
|
|
1277
|
+
|
|
1278
|
+
def _dismiss():
|
|
1279
|
+
Melty.popover_focused_ds = None
|
|
1280
|
+
_dd_close(state)
|
|
1281
|
+
draw_state.invalidate()
|
|
1282
|
+
request_render()
|
|
1283
|
+
|
|
1284
|
+
if changed:
|
|
1285
|
+
label = _dd_label_for_path(collection, _dd_as_tuple(state._picked_path))
|
|
1286
|
+
_dismiss()
|
|
1287
|
+
if picked is INSPECT:
|
|
1288
|
+
return INSPECT
|
|
1289
|
+
if callable(picked):
|
|
1290
|
+
picked()
|
|
1291
|
+
return label
|
|
1292
|
+
|
|
1293
|
+
if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
|
|
1294
|
+
_dismiss()
|
|
1295
|
+
return None
|
|
1296
|
+
|
|
1297
|
+
# Click-outside dismissal - the MENU is the inside (a click back to the
|
|
1298
|
+
# view header closes it too, as a native context menu does).
|
|
1299
|
+
if imgui.is_mouse_clicked(0):
|
|
1300
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
1301
|
+
under = Core.melty.bvh_query(mouse_x, mouse_y)
|
|
1302
|
+
if not any(_ds_in_subtree(ds, menu_ds) for ds in under):
|
|
1303
|
+
_dismiss()
|
|
1304
|
+
return None
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
@render_func(use_cache=False, disable_scroll=True, show_header=False,
|
|
1308
|
+
header_same_line=False, show_tint=False, show_name=False, is_tree=False)
|
|
1309
|
+
def draw_context_menu(input_value, draw_state, cursor_hover_inverted, func, unique=None, search_text='',
|
|
1310
|
+
search_active=False,
|
|
1311
|
+
enter_key_down=None, tab_state: TabState = None,
|
|
1312
|
+
menu_state: ContextMenuWindowState = None, **kwargs):
|
|
1313
|
+
from meltygui.core.conversion.cache_tree import UNSET_VALUE
|
|
1314
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1315
|
+
from meltygui.view.color_view import draw_tint_context
|
|
1316
|
+
from meltygui.view.header_view import flat_button
|
|
1317
|
+
from meltygui.view.tab_view import draw_tab_bar
|
|
1318
|
+
from meltygui.core.rendering.render_dispatch import _ancestor_call_line
|
|
1319
|
+
from meltygui.core.rendering.render_dispatch import _deferred_ancestors
|
|
1320
|
+
from meltygui.core.rendering.render_dispatch import _merged_call_stack_frames
|
|
1321
|
+
from meltygui.core.rendering.render_dispatch import _request_deferred_stacks
|
|
1322
|
+
|
|
1323
|
+
if input_value is None:
|
|
1324
|
+
return False, None
|
|
1325
|
+
context_menu_offset = input_value.context_menu_offset
|
|
1326
|
+
# imgui.text(type(input_value._input_value).__name__)
|
|
1327
|
+
imgui.set_cursor_screen_pos((imgui.get_cursor_screen_pos()[0] - 1, imgui.get_cursor_screen_pos()[1] - 18))
|
|
1328
|
+
# if up_key_pressed:
|
|
1329
|
+
# print("Up key pressed")g
|
|
1330
|
+
fa_up_arrow = ""
|
|
1331
|
+
fa_down_arrow = ""
|
|
1332
|
+
# The toolbar buttons are flat_buttons (draw-list, no @render_func
|
|
1333
|
+
# wrapper) styled to match `button` exactly: same text_value /
|
|
1334
|
+
# text_saturation / hover boosts, and the shadow lift = button's
|
|
1335
|
+
# z_offset 3 + the wrapper's shadow +1. The fill hue comes from the
|
|
1336
|
+
# tint (button's factor=1.0 makes `color` moot), so each call pushes the
|
|
1337
|
+
# tint the old button carried — the decorator's blue for the arrows.
|
|
1338
|
+
# [tint=(0.0, 0.241, 0.556)]
|
|
1339
|
+
button_default_tint = (0.0, 0.241, 0.556)
|
|
1340
|
+
|
|
1341
|
+
def _menu_button(label, view_id, height, tint=button_default_tint):
|
|
1342
|
+
style_manager = Melty.style_manager
|
|
1343
|
+
previous = style_manager.push_tint_fields(*tint[:4])
|
|
1344
|
+
try:
|
|
1345
|
+
return flat_button(label, draw_state, view_id=view_id, height=height,
|
|
1346
|
+
text_value=0.694, text_saturation=1.2,
|
|
1347
|
+
hover_text_boost=1.5, shadow_offset=4.0,
|
|
1348
|
+
event="left_mouse_down", style_manager=style_manager)
|
|
1349
|
+
finally:
|
|
1350
|
+
style_manager.pop_tint_fields(previous)
|
|
1351
|
+
|
|
1352
|
+
if input_value._parent.id is not None:
|
|
1353
|
+
if _menu_button(fa_up_arrow, f"ctx_menu_up##{unique}", height=50):
|
|
1354
|
+
input_value.context_menu_offset += 1
|
|
1355
|
+
# Nav generation; rides into the func tab's select_line guard so
|
|
1356
|
+
# EVERY arrow press re-applies the auto-selection, even when
|
|
1357
|
+
# returning to a level whose line was selected before (the previous
|
|
1358
|
+
# ds persists, and its one-shot marker would otherwise skip it).
|
|
1359
|
+
input_value._scope_nav_seq = getattr(input_value, "_scope_nav_seq", 0) + 1
|
|
1360
|
+
Core.melty.cache.invalidate_up(draw_state._tile_id, max_depth=5)
|
|
1361
|
+
Core.melty.cache.invalidate_up(input_value._tile_id, max_depth=5)
|
|
1362
|
+
|
|
1363
|
+
imgui.same_line()
|
|
1364
|
+
if input_value.context_menu_offset > 0:
|
|
1365
|
+
if _menu_button(fa_down_arrow, f"ctx_menu_down##{unique}", height=50):
|
|
1366
|
+
input_value.context_menu_offset = max(0, input_value.context_menu_offset - 1)
|
|
1367
|
+
input_value._scope_nav_seq = getattr(input_value, "_scope_nav_seq", 0) + 1
|
|
1368
|
+
Core.melty.cache.invalidate_up(draw_state._tile_id, max_depth=5)
|
|
1369
|
+
Core.melty.cache.invalidate_up(input_value._tile_id, max_depth=5)
|
|
1370
|
+
else:
|
|
1371
|
+
imgui.dummy(30, 30)
|
|
1372
|
+
|
|
1373
|
+
imgui.same_line()
|
|
1374
|
+
imgui.text_colored(f"{context_menu_offset}", 1, 1, 1, 0.3)
|
|
1375
|
+
imgui.same_line()
|
|
1376
|
+
|
|
1377
|
+
# Screenshot this menu's parent view, top of the menu below the nav arrows.
|
|
1378
|
+
# Deferred so the menu isn't in the shot: front the owning window (so the
|
|
1379
|
+
# view is visible), queue the view capture, hide this menu (asking to reopen
|
|
1380
|
+
# it afterward), then let screenshot.process_take_screenshot_flags grab the
|
|
1381
|
+
# view's shot a few frames later, open the shot in nemo, and reopen the menu.
|
|
1382
|
+
if _menu_button(f" ", f"screenshot_window##{unique}", height=30, tint=(0, 0, 0, 1.0)):
|
|
1383
|
+
from meltygui.core.graphics.screenshot import request_view_capture
|
|
1384
|
+
|
|
1385
|
+
def _open_in_nemo(shot_path):
|
|
1386
|
+
# Full path + close_fds=False => posix_spawn, not fork (forking this
|
|
1387
|
+
# CUDA/GL process stalls the render thread).
|
|
1388
|
+
import shutil, subprocess
|
|
1389
|
+
nemo = shutil.which("nemo") or "/usr/bin/nemo"
|
|
1390
|
+
subprocess.Popen([nemo, shot_path], close_fds=False)
|
|
1391
|
+
|
|
1392
|
+
view_ds = input_value # the view this menu is for (offset-walked)
|
|
1393
|
+
Core.melty.move_window_to_front(view_ds.root_window)
|
|
1394
|
+
draw_state._reopen = True
|
|
1395
|
+
request_view_capture(view_ds, Core.melty.frame_count, reopen_menu_ds=draw_state,
|
|
1396
|
+
on_captured=_open_in_nemo)
|
|
1397
|
+
draw_state.closed = True
|
|
1398
|
+
request_render()
|
|
1399
|
+
|
|
1400
|
+
imgui.same_line()
|
|
1401
|
+
|
|
1402
|
+
# Same deferred screenshot, then hand it to Claude: once the shot lands,
|
|
1403
|
+
# boot a fresh claude-d session pre-typed (NOT sent) with the shot path +
|
|
1404
|
+
# the view's render function (the same function the Input tab edits), and
|
|
1405
|
+
# open the Claude Terminals window so the new session's terminal comes up.
|
|
1406
|
+
if _menu_button(f" claude", f"claude_session##{unique}", height=30, tint=(0, 0, 0, 1.0)):
|
|
1407
|
+
from meltygui.core.graphics.screenshot import request_view_capture
|
|
1408
|
+
view_ds = input_value
|
|
1409
|
+
# Resolve the menu's offset-walked target so the shot + function match
|
|
1410
|
+
# what the tabs will show (the walk proper happens above the buttons).
|
|
1411
|
+
for _ in range(context_menu_offset):
|
|
1412
|
+
if view_ds._parent is None or view_ds._parent is view_ds:
|
|
1413
|
+
break
|
|
1414
|
+
view_ds = view_ds._parent
|
|
1415
|
+
fn = inspect.unwrap(view_ds._view_func)
|
|
1416
|
+
fn_name = getattr(fn, "__name__", "?")
|
|
1417
|
+
try:
|
|
1418
|
+
fn_file = inspect.getsourcefile(fn)
|
|
1419
|
+
except Exception:
|
|
1420
|
+
fn_file = None
|
|
1421
|
+
fn_line = getattr(getattr(fn, "__code__", None), "co_firstlineno", None)
|
|
1422
|
+
loc = f"{fn_file}:{fn_line}" if fn_file else "unknown location"
|
|
1423
|
+
|
|
1424
|
+
def _start_claude(shot_path, _name=fn_name, _loc=loc):
|
|
1425
|
+
from meltygui.core.services.claude_terminal_core import launch_claude_session
|
|
1426
|
+
from meltygui.core.services.claude_terminal_core import open_claude_terminals_window
|
|
1427
|
+
launch_claude_session(
|
|
1428
|
+
f"Take a look at this screenshot of a view in the studio: {shot_path} "
|
|
1429
|
+
f"It is rendered by the function `{_name}` in {_loc}. ")
|
|
1430
|
+
open_claude_terminals_window()
|
|
1431
|
+
|
|
1432
|
+
Core.melty.move_window_to_front(view_ds.root_window)
|
|
1433
|
+
draw_state._reopen = True
|
|
1434
|
+
request_view_capture(view_ds, Core.melty.frame_count, reopen_menu_ds=draw_state,
|
|
1435
|
+
on_captured=_start_claude)
|
|
1436
|
+
draw_state.closed = True
|
|
1437
|
+
request_render()
|
|
1438
|
+
|
|
1439
|
+
imgui.same_line()
|
|
1440
|
+
|
|
1441
|
+
# Recapture the caller trace: the stack - and everything riding it
|
|
1442
|
+
# (caller f_locals types + stack-snapshot live values, the target's
|
|
1443
|
+
# in-scope publish, the one-shot body-locals capture) - is grabbed
|
|
1444
|
+
# ONCE per session and kept until restart. This button-arms the one-shot
|
|
1445
|
+
# gates so the target's next inline render captures fresh, and
|
|
1446
|
+
# invalidates up so the ancestors actually re-render: a cache-replayed
|
|
1447
|
+
# target renders without its parents on the stack, which would capture
|
|
1448
|
+
# a chain that bottoms out in dispatch machinery.
|
|
1449
|
+
bug_icon = "" # fa-bug - red as a debug affordance
|
|
1450
|
+
if _menu_button(f"{bug_icon}", f"recapture_trace##{unique}", height=30,
|
|
1451
|
+
tint=(0.42, 0.24, 0.06, 1.0)):
|
|
1452
|
+
_rc_target = input_value
|
|
1453
|
+
for _ in range(context_menu_offset):
|
|
1454
|
+
if _rc_target._parent is None or _rc_target._parent is _rc_target:
|
|
1455
|
+
break
|
|
1456
|
+
_rc_target = _rc_target._parent
|
|
1457
|
+
_rc_target._call_site_captured = False
|
|
1458
|
+
_rc_target._call_site_requested = True
|
|
1459
|
+
# Every deferred layer on the chain recaptures its queue-time stack
|
|
1460
|
+
# too, so the Code tab's splice is built from fresh parts.
|
|
1461
|
+
for _deferred in _deferred_ancestors(_rc_target):
|
|
1462
|
+
_deferred._deferred_call_stack_frames = None
|
|
1463
|
+
_deferred._deferred_stack_requested = True
|
|
1464
|
+
Core.melty.cache.invalidate_up(_deferred._tile_id, max_depth=5)
|
|
1465
|
+
Core.melty.cache.invalidate_up(_rc_target._tile_id, max_depth=5)
|
|
1466
|
+
request_render()
|
|
1467
|
+
|
|
1468
|
+
imgui.same_line()
|
|
1469
|
+
|
|
1470
|
+
offset_ds = input_value
|
|
1471
|
+
for i in range(context_menu_offset):
|
|
1472
|
+
if offset_ds._parent is None:
|
|
1473
|
+
break
|
|
1474
|
+
offset_ds = offset_ds._parent
|
|
1475
|
+
|
|
1476
|
+
# The menu walked up to an ancestor (offset > 0). That ancestor never had its
|
|
1477
|
+
# OWN context menu open, so the context_menu_open capture gate never ran for
|
|
1478
|
+
# it and its _call_site is None — caller lenses come up empty. Ask its next
|
|
1479
|
+
# inline render to capture the site (lazy, one-shot), and invalidate it so it
|
|
1480
|
+
# re-renders fresh rather than from cache (where the capture line is skipped).
|
|
1481
|
+
if (offset_ds is not input_value and not offset_ds._call_site_captured
|
|
1482
|
+
and not offset_ds._call_site_requested):
|
|
1483
|
+
offset_ds._call_site_requested = True
|
|
1484
|
+
if Core.melty.cache is not None:
|
|
1485
|
+
Core.melty.cache.invalidate_up(offset_ds._tile_id, max_depth=5)
|
|
1486
|
+
request_render()
|
|
1487
|
+
# Same lazy one-shot for the queue-time stacks of every deferred layers
|
|
1488
|
+
# above the target: the Code tab splices them onto the target's stack.
|
|
1489
|
+
_request_deferred_stacks(input_value)
|
|
1490
|
+
|
|
1491
|
+
# Scope-up auto-select: when the menu is walked up to an ancestor, resolve
|
|
1492
|
+
# the line inside the ancestor's view function that drew the ORIGINAL
|
|
1493
|
+
# element (from the original target's cached _call_stack, available
|
|
1494
|
+
# immediately, no waiting on the ancestor's lazy capture above), and have
|
|
1495
|
+
# the func tab select it.
|
|
1496
|
+
func_tab_select_line = None
|
|
1497
|
+
if offset_ds is not input_value:
|
|
1498
|
+
func_tab_select_line = _ancestor_call_line(input_value, offset_ds)
|
|
1499
|
+
# Arrow-press generation: part of the selection's one-shot key, so each
|
|
1500
|
+
# press re-selects but keeping the resolved line (and editor tab) changes.
|
|
1501
|
+
func_tab_select_seq = getattr(input_value, "_scope_nav_seq", 0)
|
|
1502
|
+
|
|
1503
|
+
input_value._offset_ds = offset_ds
|
|
1504
|
+
input_value = offset_ds
|
|
1505
|
+
|
|
1506
|
+
# Font awesome info icon unicode: \uf05a
|
|
1507
|
+
gear_icon = f"\uf013"
|
|
1508
|
+
config_icon_fa = f"{gear_icon} Config"
|
|
1509
|
+
info_icon_fa = " Info"
|
|
1510
|
+
view_func_name = offset_ds._view_func.__name__
|
|
1511
|
+
class_name = type(input_value._raw_input_value).__name__
|
|
1512
|
+
paint_brush_icon = f"\uf1fc"
|
|
1513
|
+
tint_tab_name = f"{paint_brush_icon} Tint"
|
|
1514
|
+
terminal_icon = f"\uf120" # fa-terminal
|
|
1515
|
+
eval_tab_name = f"{terminal_icon} Eval"
|
|
1516
|
+
keyboard_icon = f"\uf11c" # fa-keyboard
|
|
1517
|
+
input_tab_name = f"{keyboard_icon} Input"
|
|
1518
|
+
# Lightning bolt
|
|
1519
|
+
live_icon = f"\uf0e7"
|
|
1520
|
+
live_tab = f"{live_icon} Live"
|
|
1521
|
+
code_stack_icon = f"\uf121"
|
|
1522
|
+
code_stack_tab = f"{code_stack_icon} Code"
|
|
1523
|
+
|
|
1524
|
+
# Resolve which class's source to show in the class tab.
|
|
1525
|
+
# For a non-primitive value that's just the value's own class. For a
|
|
1526
|
+
# primitive field (e.g. a float `rank`) the value itself has no source,
|
|
1527
|
+
# so walk up the parent chain to the nearest object that does have source
|
|
1528
|
+
# code -- so e.g. Lora.rank still shows Lora's class, labelled as parent.
|
|
1529
|
+
raw_value = input_value._raw_input_value
|
|
1530
|
+
class_to_show = None
|
|
1531
|
+
class_is_parent = False
|
|
1532
|
+
# A bubbling tree node's type is a runtime-generated `Bubbling_<Base>` with no source
|
|
1533
|
+
# — resolve its real base (e.g. GeneralParse) so the Class/Decorations tabs resolve
|
|
1534
|
+
# rather than erroring.
|
|
1535
|
+
from meltygui.core.conversion.bubbling import base_of_bubbling
|
|
1536
|
+
# Use exact-type matching, not isinstance: a subclass of a primitive
|
|
1537
|
+
# (e.g. CodeLine(str)) DOES have its own source, so it should show its
|
|
1538
|
+
# own class tab rather than being treated as a bare primitive.
|
|
1539
|
+
if isinstance(raw_value, type):
|
|
1540
|
+
# The view is a CLASS itself (e.g. the Toggles window draws the
|
|
1541
|
+
# Toggles class via @window). The class whose source to show is the
|
|
1542
|
+
# value, not its metaclass - type(Toggles) is `type`, a builtin with
|
|
1543
|
+
# no source, which would blank every right-side source row (class
|
|
1544
|
+
# var / @defaults / @window on the class).
|
|
1545
|
+
class_to_show = base_of_bubbling(raw_value)
|
|
1546
|
+
elif type(raw_value) not in (int, float, str, bool):
|
|
1547
|
+
class_to_show = base_of_bubbling(type(raw_value))
|
|
1548
|
+
else:
|
|
1549
|
+
max_walk = 4
|
|
1550
|
+
ancestor = input_value._parent
|
|
1551
|
+
while ancestor is not None and max_walk > 0:
|
|
1552
|
+
a_raw = getattr(ancestor, '_raw_input_value', UNSET_VALUE)
|
|
1553
|
+
a_type = base_of_bubbling(type(a_raw)) if a_raw is not UNSET_VALUE else None
|
|
1554
|
+
if a_type is not None and getattr(a_type, '__module__', None) \
|
|
1555
|
+
not in (None, 'builtins', '_collections_abc'):
|
|
1556
|
+
class_to_show = a_type
|
|
1557
|
+
class_is_parent = True
|
|
1558
|
+
break
|
|
1559
|
+
ancestor = ancestor._parent
|
|
1560
|
+
max_walk -= 1
|
|
1561
|
+
|
|
1562
|
+
# Font awesome: fa-code () for the view function, fa-cube () for the class.
|
|
1563
|
+
func_tab = f" {view_func_name}"
|
|
1564
|
+
if class_to_show is not None:
|
|
1565
|
+
class_tab = f" {class_to_show.__name__}" + (" (parent)" if class_is_parent else "")
|
|
1566
|
+
else:
|
|
1567
|
+
class_tab = f" {class_name}"
|
|
1568
|
+
|
|
1569
|
+
# Static tint colors for the fixed Config / Info tabs; remaining tabs use the neutral grey.
|
|
1570
|
+
config_tint = (0.12, 0.38, 0.772)
|
|
1571
|
+
info_tint = (0.545, 0.469, 0.012)
|
|
1572
|
+
|
|
1573
|
+
tab_names = []
|
|
1574
|
+
tab_tints = []
|
|
1575
|
+
tab_names.append(info_icon_fa)
|
|
1576
|
+
tab_tints.append(info_tint)
|
|
1577
|
+
tab_names.append(config_icon_fa)
|
|
1578
|
+
tab_tints.append(config_tint)
|
|
1579
|
+
tab_names.append(func_tab)
|
|
1580
|
+
tab_tints.append(None)
|
|
1581
|
+
tab_names.append(eval_tab_name)
|
|
1582
|
+
tab_tints.append((0.2, 0.7, 0.3)) # green for the eval/REPL tab
|
|
1583
|
+
tab_names.append(input_tab_name)
|
|
1584
|
+
tab_tints.append((0.4, 0.2, 0.7)) # purple for the input tab
|
|
1585
|
+
tab_names.append(tint_tab_name)
|
|
1586
|
+
tab_tints.append(Core.melty._saturated_rgb(draw_state.tint)) # orange tint for the tint tab
|
|
1587
|
+
if class_to_show is not None:
|
|
1588
|
+
tab_names.append(class_tab)
|
|
1589
|
+
tab_tints.append(None)
|
|
1590
|
+
|
|
1591
|
+
tab_names.append(live_tab)
|
|
1592
|
+
tab_tints.append((0.7, 0.0, 0.0))
|
|
1593
|
+
tab_names.append(code_stack_tab)
|
|
1594
|
+
tab_tints.append((0.9, 0.35, 0.28)) # the code trace view's own tint
|
|
1595
|
+
|
|
1596
|
+
indices = list(range(len(tab_names)))
|
|
1597
|
+
|
|
1598
|
+
if not tab_state.selected_tabs:
|
|
1599
|
+
tab_state.selected_tabs = [indices[Toggles.ContextMenu.default_tab]]
|
|
1600
|
+
|
|
1601
|
+
current_mode = input_value._kwargs.get('mode', None)
|
|
1602
|
+
mode_tab = str(current_mode)
|
|
1603
|
+
if current_mode is not None:
|
|
1604
|
+
tab_names.append(mode_tab)
|
|
1605
|
+
|
|
1606
|
+
# Indices list
|
|
1607
|
+
|
|
1608
|
+
imgui.dummy(0, 1)
|
|
1609
|
+
imgui.same_line()
|
|
1610
|
+
tab_changed, new_tabs = draw_tab_bar(tab_state.selected_tabs, names=tab_names, wrap=True, tab_height=40,
|
|
1611
|
+
tint_value=0.7,
|
|
1612
|
+
# 282 = nav arrows + counter + shot +
|
|
1613
|
+
# claude; +46 for the + (trace
|
|
1614
|
+
# recapture) button.
|
|
1615
|
+
width=max(50, draw_state.content_width - 328),
|
|
1616
|
+
show_bg=True, name=f"tab_bar#{view_func_name}{unique}",
|
|
1617
|
+
z_offset=-0.5, bg_offset=-7, draw=True,
|
|
1618
|
+
collection=indices, tints=tab_tints, as_toggles=False)
|
|
1619
|
+
if tab_changed:
|
|
1620
|
+
tab_state.selected_tabs = new_tabs
|
|
1621
|
+
# The compact input tab's initial fit leaves no room for source rows.
|
|
1622
|
+
# Give Inputs a usable viewport when selecting it from that small fit.
|
|
1623
|
+
if tab_names.index(input_tab_name) in new_tabs:
|
|
1624
|
+
minimum_height = min(700, int(imgui.get_io().display_size.y * 0.8))
|
|
1625
|
+
if (draw_state.height or 0) < minimum_height:
|
|
1626
|
+
draw_state.height = minimum_height
|
|
1627
|
+
draw_state.invalidate()
|
|
1628
|
+
|
|
1629
|
+
imgui.dummy(0, 2)
|
|
1630
|
+
|
|
1631
|
+
# Columns stripped: render the selected tab(s) stacked at the menu's full
|
|
1632
|
+
# width - one all across the menu, several fill the height. No Column calls,
|
|
1633
|
+
# so nothing here triggers the window-frame edge system (the source of the
|
|
1634
|
+
n_sel = max(1, len(tab_state.selected_tabs))
|
|
1635
|
+
full_w = draw_state.content_width
|
|
1636
|
+
clip = draw_state.abs_clip_rect
|
|
1637
|
+
top_y = imgui.get_cursor_screen_pos()[1]
|
|
1638
|
+
# First-load auto-fit (same pattern as draw_global_search's height
|
|
1639
|
+
# auto-fit): until the menu has been fitted once, hand the tab bodies NO
|
|
1640
|
+
# height so they render at their natural extent - the usual tab_h derives
|
|
1641
|
+
# from the current window clip rect which is circular while we're still
|
|
1642
|
+
# choosing the window height. fit_done PERSISTS with the menu's
|
|
1643
|
+
# draw_state (ContextMenuWindowState): a menu restored open at boot
|
|
1644
|
+
# already had its size, and re-fitting it overwrote that size.
|
|
1645
|
+
fitting = not menu_state.fit_done
|
|
1646
|
+
tab_h = max(60.0, (clip[3] - top_y) / n_sel - 6) if (clip is not None and not fitting) else None
|
|
1647
|
+
|
|
1648
|
+
for t_idx, static_tab in enumerate(tab_state.selected_tabs):
|
|
1649
|
+
size_kw = {"width": full_w}
|
|
1650
|
+
if tab_h is not None:
|
|
1651
|
+
size_kw["height"] = tab_h
|
|
1652
|
+
if static_tab >= len(tab_names):
|
|
1653
|
+
# A persisted selection that outran the current tab set: fall back
|
|
1654
|
+
# to the func tab rather than indexing out of range.
|
|
1655
|
+
draw_func_tab(input_value, name=f"func_tab_{t_idx}##{unique}",
|
|
1656
|
+
disable_scroll=True, select_line=func_tab_select_line,
|
|
1657
|
+
select_seq=func_tab_select_seq, **size_kw)
|
|
1658
|
+
continue
|
|
1659
|
+
this_tab = tab_names[static_tab]
|
|
1660
|
+
if this_tab == tint_tab_name:
|
|
1661
|
+
draw_tint_context(input_value, name=f"Context Tint##{unique}", **size_kw)
|
|
1662
|
+
|
|
1663
|
+
elif this_tab == info_icon_fa:
|
|
1664
|
+
# Resolve the effective search term (menu kwarg, else its search box).
|
|
1665
|
+
info_search = search_text if search_text != "" else draw_state.search_text
|
|
1666
|
+
draw_info_tab(input_value, search_text=info_search, unique=unique,
|
|
1667
|
+
name=f"info_tab_{t_idx}##{unique}", **size_kw)
|
|
1668
|
+
|
|
1669
|
+
elif this_tab == config_icon_fa:
|
|
1670
|
+
draw_config_tab(input_value, name=f"config_tab_{t_idx}##{unique}", **size_kw)
|
|
1671
|
+
|
|
1672
|
+
elif this_tab == func_tab:
|
|
1673
|
+
draw_func_tab(input_value, name=f"func_tab_{t_idx}##{unique}",
|
|
1674
|
+
disable_scroll=False, select_line=func_tab_select_line,
|
|
1675
|
+
select_seq=func_tab_select_seq, **size_kw)
|
|
1676
|
+
|
|
1677
|
+
elif this_tab == eval_tab_name:
|
|
1678
|
+
draw_eval_tab(input_value, unique=unique, enter_key_down=enter_key_down,
|
|
1679
|
+
menu_draw_state=draw_state,
|
|
1680
|
+
name=f"eval_tab_{t_idx}##{unique}", **size_kw)
|
|
1681
|
+
|
|
1682
|
+
elif this_tab == input_tab_name:
|
|
1683
|
+
draw_input_tab(input_value, class_to_show=class_to_show,
|
|
1684
|
+
name=f"input_tab_{t_idx}##{unique}", wrap=False,
|
|
1685
|
+
disable_scroll=False, **size_kw)
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
elif this_tab == class_tab:
|
|
1689
|
+
draw_class_tab(input_value, class_to_show=class_to_show, class_is_parent=class_is_parent,
|
|
1690
|
+
class_name=class_name, name=f"class_tab_{t_idx}##{unique}", **size_kw)
|
|
1691
|
+
|
|
1692
|
+
elif this_tab == mode_tab:
|
|
1693
|
+
draw_mode_tab(input_value, current_mode=current_mode,
|
|
1694
|
+
name=f"mode_tab_{t_idx}##{unique}", **size_kw)
|
|
1695
|
+
|
|
1696
|
+
elif this_tab == live_tab:
|
|
1697
|
+
draw_live_tab(input_value, name=f"live_tab_{t_idx}##{unique}", **size_kw)
|
|
1698
|
+
|
|
1699
|
+
elif this_tab == code_stack_tab:
|
|
1700
|
+
# The stack captured at menu open (core_render's one-shot grab -
|
|
1701
|
+
# `_call_stack_frames`: (path, lineno, func_name, locals) tuples,
|
|
1702
|
+
# outermost first) with its deferred ancestor's queue-time
|
|
1703
|
+
# stack spliced in (_merged_call_stack_frames), rendered as a
|
|
1704
|
+
# stack trace view. Values come from the frames' captured locals
|
|
1705
|
+
# through pane-LOCAL stores - nothing published, nothing global.
|
|
1706
|
+
# The debug (bug) button above recaptures a fresh stack.
|
|
1707
|
+
from meltygui.view.trace_view import draw_stack_trace
|
|
1708
|
+
captured_stack = _merged_call_stack_frames(input_value, menu_state)
|
|
1709
|
+
if captured_stack:
|
|
1710
|
+
# indent_views=False: the tab is narrow - panes slide left
|
|
1711
|
+
# instead of the inlined call-chain slide.
|
|
1712
|
+
draw_stack_trace(
|
|
1713
|
+
captured_stack, indent_views=False,
|
|
1714
|
+
hide_dispatch=Toggles.ContextMenu.code_tab_hide_dispatch,
|
|
1715
|
+
name=f"code_tab_{t_idx}##{unique}", **size_kw)
|
|
1716
|
+
else:
|
|
1717
|
+
RenderFuncs.draw_text(
|
|
1718
|
+
"No captured stack for this view yet — press the bug "
|
|
1719
|
+
"button above to recapture the trace.",
|
|
1720
|
+
name=f"code_tab_empty_{t_idx}##{unique}", show_bg=False,
|
|
1721
|
+
editable=False, tint=Tint.subtle_text())
|
|
1722
|
+
imgui.set_cursor_screen_pos((imgui.get_cursor_screen_pos()[0] - 1, imgui.get_cursor_screen_pos()[1] - 18))
|
|
1723
|
+
imgui.text(f"{input_value._raw_input_value.__class__.__name__}")
|
|
1724
|
+
|
|
1725
|
+
imgui.dummy(0, 30)
|
|
1726
|
+
|
|
1727
|
+
# First-load fit, two phases. Phase 0: set the default width - width
|
|
1728
|
+
# drives wrap, so a height measure is only honest once the content has
|
|
1729
|
+
# rendered AT that width; invalidate unconditionally (use_cache would
|
|
1730
|
+
# otherwise replay the tile and skip phase 1). Phase 1: the tab bodies
|
|
1731
|
+
# just rendered at no height, so the cursor bottom IS the content
|
|
1732
|
+
# extent. Write the size immediately - the wrapper's measured item_rect
|
|
1733
|
+
# can't shrink a fixed-width window - cap at most of the display, and fit
|
|
1734
|
+
# ONCE per menu draw_state: the ds persists across open/close, so reopens
|
|
1735
|
+
# and tab switches keep the user's size.
|
|
1736
|
+
if fitting:
|
|
1737
|
+
if menu_state.fit_phase == 0:
|
|
1738
|
+
menu_state.fit_phase = 1
|
|
1739
|
+
if (draw_state.width or 0) < 520:
|
|
1740
|
+
draw_state.width = 520
|
|
1741
|
+
draw_state._source["width"] = "context menu first-load default width"
|
|
1742
|
+
draw_state.invalidate()
|
|
1743
|
+
request_render()
|
|
1744
|
+
else:
|
|
1745
|
+
content_bottom = imgui.get_cursor_screen_pos()[1]
|
|
1746
|
+
new_h = max(100, int(content_bottom - draw_state._abs_top() + 10))
|
|
1747
|
+
disp_h = imgui.get_io().display_size.y
|
|
1748
|
+
if disp_h > 0:
|
|
1749
|
+
new_h = min(new_h, int(disp_h * 0.8))
|
|
1750
|
+
if draw_state.height is None or abs(draw_state.height - new_h) > 1:
|
|
1751
|
+
draw_state.height = new_h
|
|
1752
|
+
draw_state._source["height"] = "context menu first-load auto-fit"
|
|
1753
|
+
draw_state.invalidate()
|
|
1754
|
+
request_render()
|
|
1755
|
+
menu_state.fit_done = True
|
|
1756
|
+
|
|
1757
|
+
# Never open the menu partially off-display. While the window offset is
|
|
1758
|
+
# still the fresh default reset (0,0) - core_render does that on every
|
|
1759
|
+
# right-click open - shift window_pos (the additive offset in the pinned
|
|
1760
|
+
# branch of _abs_left/_abs_top) so the whole window fits inside the
|
|
1761
|
+
# display. A user drag writes window_pos and ends this clamping; blit
|
|
1762
|
+
# placement follows abs pos anyway, so no invalidate is needed for a move.
|
|
1763
|
+
wp = draw_state.window_pos or (0, 0)
|
|
1764
|
+
if not fitting and tuple(wp) == (0, 0):
|
|
1765
|
+
disp = imgui.get_io().display_size
|
|
1766
|
+
x0, y0 = draw_state._abs_left(), draw_state._abs_top()
|
|
1767
|
+
w = draw_state.width or draw_state.content_width or 0
|
|
1768
|
+
h = draw_state.height or 0
|
|
1769
|
+
dx = dy = 0.0
|
|
1770
|
+
if disp.x > 0:
|
|
1771
|
+
if x0 + w > disp.x:
|
|
1772
|
+
dx = disp.x - (x0 + w)
|
|
1773
|
+
if x0 + dx < 0:
|
|
1774
|
+
dx = -x0
|
|
1775
|
+
if disp.y > 0:
|
|
1776
|
+
if y0 + h > disp.y:
|
|
1777
|
+
dy = disp.y - (y0 + h)
|
|
1778
|
+
if y0 + dy < 0:
|
|
1779
|
+
dy = -y0
|
|
1780
|
+
if abs(dx) > 0.5 or abs(dy) > 0.5:
|
|
1781
|
+
draw_state.window_pos = (wp[0] + dx, wp[1] + dy)
|
|
1782
|
+
request_render()
|
|
1783
|
+
return False, input_value
|