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,1599 @@
|
|
|
1
|
+
"""Reusable file presentation; values and view state are supplied by callers."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from meltygui.core.runtime.paths import debug_log_path
|
|
4
|
+
from meltygui.core.files.file_explorer_core import _trace_browser_size
|
|
5
|
+
from meltygui.core.rendering.modes import Modes
|
|
6
|
+
from meltygui.core.core_render import render_func
|
|
7
|
+
from meltygui.core.rendering.render_funcs import RenderFuncs
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from meltygui.hdr_color import pack_color
|
|
10
|
+
from meltygui.hdr_color import with_alpha
|
|
11
|
+
from meltygui.core.melty import Melty
|
|
12
|
+
from meltygui.state.file_state import FileExplorerState
|
|
13
|
+
from meltygui.state.file_state import FileSelectorState
|
|
14
|
+
from meltygui.state.file_state import FileTreeState
|
|
15
|
+
from meltygui.state.file_state import ShortcutState
|
|
16
|
+
from meltygui.core.runtime.toggles import Tint
|
|
17
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
18
|
+
from meltygui.state.file_state import ROOT
|
|
19
|
+
import colorsys
|
|
20
|
+
import difflib
|
|
21
|
+
import meltygui_imgui as imgui
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@render_func(tint=(0.36, 0.46, 0.59), show_bg=True, selectable=False)
|
|
27
|
+
def draw_file_tree(input_value: dict, draw_state, root: Path = None):
|
|
28
|
+
"""Edit a held directory tree without owning its I/O or host envelope."""
|
|
29
|
+
return RenderFuncs.draw_collection(
|
|
30
|
+
input_value, name=root.name if root is not None else "Files",
|
|
31
|
+
show_add_delete=True, new_item_type=str, temp=True,
|
|
32
|
+
width=draw_state.content_width, disable_scroll=True, show_bg=True,
|
|
33
|
+
child_kwargs={"show_bg": True, "bg_offset": -4,
|
|
34
|
+
"child_kwargs": {"show_bg": False, "is_tree": False,
|
|
35
|
+
"expanded": True},
|
|
36
|
+
"mode": Modes.FILE_TREE})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@render_func(tint=(0.18, 0.11, 0.11), selectable=False)
|
|
40
|
+
def draw_file_metadata(input_value: dict):
|
|
41
|
+
"""Edit the supplied metadata mapping without looking up global stores."""
|
|
42
|
+
return RenderFuncs.draw_collection(input_value, name="file_meta", is_tree=True,
|
|
43
|
+
show_add_delete=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@render_func()
|
|
48
|
+
def draw_external_changes(draw_state=None):
|
|
49
|
+
from meltygui.editor.external_changes import ExternalChanges
|
|
50
|
+
from meltygui.editor.pending_save import _diff_lines_with_numbers
|
|
51
|
+
|
|
52
|
+
from meltygui.core.melty import Melty
|
|
53
|
+
from meltygui.core.melty import FileWatch
|
|
54
|
+
ExternalChanges._window_ds = draw_state
|
|
55
|
+
RenderFuncs.draw_function(ExternalChanges.dismiss_all, tint=(0, 0, 0, 1), show_bg=False, shadow=False, icon=None)
|
|
56
|
+
|
|
57
|
+
# DEBUG: what the tracker will actually see. A file only gets a baseline if
|
|
58
|
+
# its text sat in Melty.code_cache when the fs event fired (old_lines is the
|
|
59
|
+
# popped cache entry), so per watched file show whether it's cached NOW -
|
|
60
|
+
# "no" means an external edit to it would be skipped silently.
|
|
61
|
+
watched = sorted(FileWatch.path_to_draw_states.keys())
|
|
62
|
+
lines = [f"tracked entries: {len(ExternalChanges.originals)}"]
|
|
63
|
+
for p, orig in sorted(ExternalChanges.originals.items()):
|
|
64
|
+
lines.append(f" {p} (baseline {len(str(orig).splitlines())} lines)")
|
|
65
|
+
lines.append(f"watched dirs: {len(FileWatch._watched_dirs)} "
|
|
66
|
+
f"code_cache entries: {len(Melty.code_cache)}")
|
|
67
|
+
lines.append(f"watched files ({len(watched)}):")
|
|
68
|
+
for p in watched:
|
|
69
|
+
cached = "cached" if p in Melty.code_cache else "NOT in code_cache"
|
|
70
|
+
lines.append(f" {p} [{len(FileWatch.path_to_draw_states[p])} views, {cached}]")
|
|
71
|
+
RenderFuncs.draw_text("\n".join(lines), show_name=True, name="watch debug")
|
|
72
|
+
|
|
73
|
+
entries = []
|
|
74
|
+
for path, original in list(ExternalChanges.originals.items()):
|
|
75
|
+
# Once the studio itself writes the file, disk equals in-process truth
|
|
76
|
+
# again - the external change is resolved, drop the entry. This
|
|
77
|
+
# also self-heals the truncate race: a mid-write fs event can slip
|
|
78
|
+
# past the event-time is_self_write check (disk hash not final yet)
|
|
79
|
+
# and record a bogus entry, but by render time the flush has landed.
|
|
80
|
+
if FileWatch.is_self_write(path):
|
|
81
|
+
with open(debug_log_path("ext_changes_debug.log"), "a") as _f:
|
|
82
|
+
_f.write(f"pop self_write {path} recorded={FileWatch._self_write_hashes.get(path)} "
|
|
83
|
+
f"disk={FileWatch._get_hash(path)}\n")
|
|
84
|
+
ExternalChanges.untrack(path)
|
|
85
|
+
continue
|
|
86
|
+
current = Melty.read_code(path)
|
|
87
|
+
if current is not None and \
|
|
88
|
+
current.splitlines(keepends=True) == str(original).splitlines(keepends=True):
|
|
89
|
+
# Drifted back to the baseline (e.g. an outside edit was undone).
|
|
90
|
+
# Only heal when no absorb moved the SYNC frame past the baseline:
|
|
91
|
+
# after an absorb, live code is the absorbed state, so a disk
|
|
92
|
+
# change is a sync→disk drift the next recompile must absorb -
|
|
93
|
+
# untracking here would leave live and disk silently diverged.
|
|
94
|
+
_synced = ExternalChanges.synced.get(path)
|
|
95
|
+
if _synced is None or str(_synced).splitlines(keepends=True) \
|
|
96
|
+
== current.splitlines(keepends=True):
|
|
97
|
+
with open(debug_log_path("ext_changes_debug.log"), "a") as _f:
|
|
98
|
+
_f.write(f"pop drift_back {path}\n")
|
|
99
|
+
ExternalChanges.untrack(path)
|
|
100
|
+
continue
|
|
101
|
+
entries.append((path, original, current))
|
|
102
|
+
|
|
103
|
+
# Stale-blit guard. The event frame renders with the cache BYPASSED (the
|
|
104
|
+
# _external_change flag), so its output is never captured into the blit
|
|
105
|
+
# tiles - without this, the next frame replays the pre-event tile and the
|
|
106
|
+
# fresh diff flickers away. When the rendered diff actually changes,
|
|
107
|
+
# force-invalidate this window's own subtree so the tiles re-capture.
|
|
108
|
+
# id(current) is a content-free change signal: code_cache holds each text
|
|
109
|
+
# until the watcher pops it, and every re-read is a new str object. The
|
|
110
|
+
# absorption marker joins the sig so the "absorbed" annotation appearing
|
|
111
|
+
# (same texts, new baseline) still re-captures the tiles.
|
|
112
|
+
sig = tuple((p, id(c), ExternalChanges.absorbed.get(p)) for p, _, c in entries)
|
|
113
|
+
if draw_state.misc.get("_ext_sig") != sig:
|
|
114
|
+
draw_state.misc["_ext_sig"] = sig
|
|
115
|
+
Melty.cache.invalidate_up(draw_state._tile_id, force=True, max_depth=6)
|
|
116
|
+
|
|
117
|
+
for path, original, current in entries:
|
|
118
|
+
file_name = Path(path).name
|
|
119
|
+
if current is None:
|
|
120
|
+
if RenderFuncs.button(f" Dismiss##{path}", name=f"dismiss {path}",
|
|
121
|
+
tint=(0.12, 0.002037035, 0.002037035, 0.4))[0]:
|
|
122
|
+
ExternalChanges.untrack(path)
|
|
123
|
+
RenderFuncs.draw_text(f"{path}: deleted or unreadable", name=f"{file_name}##{path}")
|
|
124
|
+
continue
|
|
125
|
+
new_lines = current.splitlines(keepends=True)
|
|
126
|
+
old_lines = str(original).splitlines(keepends=True)
|
|
127
|
+
|
|
128
|
+
diff = difflib.unified_diff(
|
|
129
|
+
fromfile=path, tofile=path,
|
|
130
|
+
a=old_lines, b=new_lines, n=3,
|
|
131
|
+
)
|
|
132
|
+
# Whole-file diffs, so the @@ hunk numbers ARE the file's line numbers
|
|
133
|
+
# - base 0 (see _diff_lines_with_numbers; pending_save spans shift by
|
|
134
|
+
# their address.start, a whole file starts at 0).
|
|
135
|
+
content_lines, line_numbers = _diff_lines_with_numbers(diff, 0)
|
|
136
|
+
diff_str = "".join(content_lines)
|
|
137
|
+
|
|
138
|
+
if RenderFuncs.button(f" Dismiss##{path}", name=f"dismiss {path}",
|
|
139
|
+
tint=(0.12, 0.002037035, 0.002037035, 0.4))[0]:
|
|
140
|
+
# Dismiss = accept the on-disk state as the new baseline: the
|
|
141
|
+
# entry drops, and the next external edit re-baselines from
|
|
142
|
+
# whatever the cache holds then.
|
|
143
|
+
ExternalChanges.untrack(path)
|
|
144
|
+
if ExternalChanges.is_absorbed(path, current):
|
|
145
|
+
RenderFuncs.draw_text("absorbed into pending queue (recompiled) — "
|
|
146
|
+
"Dismiss to clear",
|
|
147
|
+
name=f"absorbed {path}", tint=(0.45, 0.75, 0.45),
|
|
148
|
+
show_bg=False)
|
|
149
|
+
RenderFuncs.draw_text(diff_str, show_name=True, name=f"{file_name}##{path}",
|
|
150
|
+
is_diff=True, line_numbers=line_numbers)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def draw_changed_file_header(path, tint, draw_state, view_id, width, height=23.0,
|
|
154
|
+
added=None, removed=None, active=False, prefix="", shadow_offset=None,
|
|
155
|
+
background=True):
|
|
156
|
+
"""Shared draw-list file header for the diff column and chat tool summaries.
|
|
157
|
+
|
|
158
|
+
`shadow_offset` overrides the tab-state lift for BOTH the wash and the
|
|
159
|
+
button (0 = flat); None keeps the CodeEditor tab offsets.
|
|
160
|
+
`background=False` draws the label only — no wash, no button bg, no
|
|
161
|
+
shadow (the files column passes it for a file with no painted tint).
|
|
162
|
+
Inactive labels use the column's own compare_file_text_* knobs, brighter
|
|
163
|
+
than the tab bar's inactive tabs: the column has no bg to read against.
|
|
164
|
+
"""
|
|
165
|
+
from meltygui.editor.file_header import _ellipsize
|
|
166
|
+
from meltygui.editor.source_ui import _tab_text_color
|
|
167
|
+
from meltygui.view.header_view import flat_button
|
|
168
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
169
|
+
|
|
170
|
+
if active:
|
|
171
|
+
text_color = _tab_text_color(tint, Toggles.CodeEditor.tab_active_text_brightness,
|
|
172
|
+
Toggles.CodeEditor.tab_active_text_saturation, Toggles.CodeEditor.tab_active_text_min_brightness)
|
|
173
|
+
brightness, saturation = Toggles.CodeEditor.tab_active_bg_brightness, Toggles.CodeEditor.tab_active_bg_saturation
|
|
174
|
+
maximum, alpha = Toggles.CodeEditor.tab_active_bg_max_brightness, 0.9
|
|
175
|
+
shadow = Toggles.CodeEditor.tab_active_shadow_offset
|
|
176
|
+
else:
|
|
177
|
+
text_color = _tab_text_color(tint, Toggles.CodeEditor.compare_file_text_brightness,
|
|
178
|
+
Toggles.CodeEditor.tab_inactive_text_saturation, Toggles.CodeEditor.compare_file_text_min_brightness)
|
|
179
|
+
brightness, saturation = Toggles.CodeEditor.tab_inactive_bg_brightness, Toggles.CodeEditor.tab_inactive_bg_saturation
|
|
180
|
+
maximum, alpha = Toggles.CodeEditor.tab_inactive_bg_max_brightness, Toggles.CodeEditor.tab_inactive_bg_alpha
|
|
181
|
+
shadow = Toggles.CodeEditor.tab_inactive_shadow_offset
|
|
182
|
+
if shadow_offset is not None:
|
|
183
|
+
shadow = shadow_offset
|
|
184
|
+
if not background:
|
|
185
|
+
alpha, shadow = 0.0, 0.0
|
|
186
|
+
x, y = imgui.get_cursor_screen_pos()
|
|
187
|
+
dl = imgui.get_window_draw_list()
|
|
188
|
+
channel = Melty.get_channel()
|
|
189
|
+
if Melty.channels_split:
|
|
190
|
+
dl.channels_set_current(channel - 1)
|
|
191
|
+
try:
|
|
192
|
+
if background:
|
|
193
|
+
wash = Melty.style_manager.make_color_rgb(*tint[:3],
|
|
194
|
+
value=Toggles.CodeEditor.compare_file_bg_value, factor=0.8, saturation_scale=1.0)
|
|
195
|
+
if shadow_offset is None or shadow_offset:
|
|
196
|
+
add_shadow((x, y, width, height), offset=1.0 if shadow_offset is None else shadow_offset, corner_radius=4.0)
|
|
197
|
+
dl.add_rect_filled(x, y, x + width, y + height,
|
|
198
|
+
pack_color(*wash[:3], 1.0), rounding=4.0)
|
|
199
|
+
clicked = flat_button("", draw_state, view_id=view_id, width=width, height=height,
|
|
200
|
+
color=tuple(tint[:3]), factor=0.1, tint_value=brightness, saturation=saturation,
|
|
201
|
+
max_bg_brightness=maximum, alpha=alpha, shadow_offset=shadow, event="left_mouse_down")
|
|
202
|
+
finally:
|
|
203
|
+
if Melty.channels_split:
|
|
204
|
+
dl.channels_set_current(channel)
|
|
205
|
+
inset = Melty.px(2)
|
|
206
|
+
counts = f"+{added} -{removed}" if added is not None and removed is not None else ""
|
|
207
|
+
size = imgui.calc_text_size(counts)
|
|
208
|
+
reserve = size.x + Melty.px(8) if counts else 0
|
|
209
|
+
label = _ellipsize((prefix + " " if prefix else "") + Path(path).name, max(0, width - inset * 2 - reserve))
|
|
210
|
+
label_size = imgui.calc_text_size(label)
|
|
211
|
+
dl.add_text(x + inset + Melty.px(2), y + (height - label_size.y) * 0.5 - Melty.px(1),
|
|
212
|
+
pack_color(*text_color[:3], 1.0), label)
|
|
213
|
+
if counts:
|
|
214
|
+
left, top = x + width - inset - size.x, y + (height - size.y) * 0.5
|
|
215
|
+
dl.add_text(left, top, pack_color(*Tint.change_count(added=True), 1.0), f"+{added}")
|
|
216
|
+
dl.add_text(left + imgui.calc_text_size(f"+{added} ").x, top,
|
|
217
|
+
pack_color(*Tint.change_count(added=False), 1.0), f"-{removed}")
|
|
218
|
+
return clicked
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@render_func()
|
|
222
|
+
def draw_pending_saves():
|
|
223
|
+
from meltygui.editor.pending_save import PendingSave
|
|
224
|
+
from meltygui.editor.pending_save import _pending_diff_memo
|
|
225
|
+
from meltygui.editor.pending_save import _render_diff_blocks
|
|
226
|
+
|
|
227
|
+
pass
|
|
228
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
229
|
+
RenderFuncs.draw_function(PendingSave.apply_all_saves, icon="", tint=(0,0,0,1), show_bg=False)
|
|
230
|
+
# name= keeps its draw_state distinct from apply_all_saves' (both calls
|
|
231
|
+
# would otherwise derive the same file-name identity); run_in_thread so
|
|
232
|
+
# the hotswaps run outside the render loop like every other recompile.
|
|
233
|
+
# result_fade_frames: the check mark + "Recompiled ..." summary hold
|
|
234
|
+
# briefly, then fade themselves (same fade model as code_file_io's
|
|
235
|
+
# recompile_status) instead of parking forever.
|
|
236
|
+
RenderFuncs.draw_function(PendingSave.recompile_all, name="recompile_all", icon="",
|
|
237
|
+
tint=(0,0,0,1), show_bg=False, run_in_thread=True,
|
|
238
|
+
result_fade_frames=30, temp=True)
|
|
239
|
+
|
|
240
|
+
# Result of the last external-change absorb (recompile_all's merge pass).
|
|
241
|
+
# Persistent, no fading if a save rewrote the file - the per-file
|
|
242
|
+
# MERGED / ADOPTED / CONFLICT outcome stays visible until dismissed.
|
|
243
|
+
if PendingSave.merge_results:
|
|
244
|
+
if RenderFuncs.button(" Dismiss##merge_results", name="dismiss merge_results",
|
|
245
|
+
tint=(0.12, 0.002037035, 0.002037035, 0.4))[0]:
|
|
246
|
+
PendingSave.merge_results = []
|
|
247
|
+
RenderFuncs.draw_text("\n".join(PendingSave.merge_results), show_name=True,
|
|
248
|
+
name="external merge results")
|
|
249
|
+
|
|
250
|
+
# Drop memo entries whose queue entry is gone (applied/re reverted).
|
|
251
|
+
for _k in list(_pending_diff_memo):
|
|
252
|
+
if _k not in PendingSave.pending_saves:
|
|
253
|
+
_pending_diff_memo.pop(_k, None)
|
|
254
|
+
for address, (codec, kwargs) in list(PendingSave.pending_saves.items()):
|
|
255
|
+
if address in PendingSave.originals:
|
|
256
|
+
original_data = PendingSave.originals[address]
|
|
257
|
+
new_data = kwargs.get("data")
|
|
258
|
+
old_data = original_data
|
|
259
|
+
# Identity-memoized diff (content-free keys - all texts are
|
|
260
|
+
# fresh objects on change): recompute only on real edges. A
|
|
261
|
+
# typing edge (same original, new pending text) splices through
|
|
262
|
+
# the incremental differ off the previous blocks; anything else
|
|
263
|
+
# runs the full matcher once. The +/-/context text and the
|
|
264
|
+
# and line numbers (base = address.start) render straight from
|
|
265
|
+
# the blocks - no unified_diff pass.
|
|
266
|
+
m = _pending_diff_memo.get(address)
|
|
267
|
+
if m is None or m[0] != id(old_data) or m[1] != id(new_data):
|
|
268
|
+
from meltygui.editor.diff import _diff_blocks
|
|
269
|
+
from meltygui.editor.diff import _incremental_diff_blocks
|
|
270
|
+
old_s, new_s = str(old_data), str(new_data)
|
|
271
|
+
blocks = None
|
|
272
|
+
if m is not None and m[0] == id(old_data) and m[2] is not None:
|
|
273
|
+
prev_new, prev_blocks = m[2]
|
|
274
|
+
blocks = _incremental_diff_blocks(old_s, prev_new, new_s,
|
|
275
|
+
prev_blocks)
|
|
276
|
+
if blocks is None:
|
|
277
|
+
blocks = _diff_blocks(old_s, new_s)
|
|
278
|
+
content_lines, line_numbers = _render_diff_blocks(
|
|
279
|
+
old_s.split("\n"), new_s.split("\n"), blocks,
|
|
280
|
+
address.start or 0)
|
|
281
|
+
m = (id(old_data), id(new_data), (new_s, blocks),
|
|
282
|
+
"".join(content_lines), line_numbers)
|
|
283
|
+
_pending_diff_memo[address] = m
|
|
284
|
+
diff_str, line_numbers = m[3], m[4]
|
|
285
|
+
if not diff_str:
|
|
286
|
+
continue # no-op edit (pending == original)
|
|
287
|
+
|
|
288
|
+
file_name = address.path.name
|
|
289
|
+
line_range = (f"({address.start}:{address.end})"
|
|
290
|
+
if address.start is not None else "(whole file)")
|
|
291
|
+
name = f"{file_name} {line_range}"
|
|
292
|
+
if RenderFuncs.button(f" Revert##{name}", name=f"revert {name}",
|
|
293
|
+
tint=(0.12, 0.002037035, 0.002037035, 0.4))[0]:
|
|
294
|
+
# Revert: queue the load-time text as a fresh pending edit.
|
|
295
|
+
# The entry stays in the queue (so sibling views still resolve
|
|
296
|
+
# their text through pending_data_for and pick up the revert),
|
|
297
|
+
# but data == original makes it a no-op for the diff view,
|
|
298
|
+
# recompile_all, and the eventual disk write.
|
|
299
|
+
PendingSave.queue_save(address, codec, **{**kwargs, "data": old_data})
|
|
300
|
+
# Deferred saves never touch disk, so no watcher wakes on its
|
|
301
|
+
# own - dispatch the file event so every view of this file
|
|
302
|
+
# reloads and picks the reverted text up from the pending cache.
|
|
303
|
+
PendingSave._wake_file_watchers(address.path)
|
|
304
|
+
RenderFuncs.draw_text(diff_str, show_name=True, name=name,
|
|
305
|
+
is_diff=True, line_numbers=line_numbers)
|
|
306
|
+
else:
|
|
307
|
+
# Baseline against DISK, never the pending cache: a plain
|
|
308
|
+
# codec.load answers with the queued edit itself, and stamping the
|
|
309
|
+
# edit as its own "original" reclassifies the entry as a no-op
|
|
310
|
+
# (dropped on the next disk write, invisible in this diff) and
|
|
311
|
+
# poisons the merge base. source_text pins the load to disk.
|
|
312
|
+
from meltygui.core.melty import Melty
|
|
313
|
+
disk_text = Melty.read_code(address.path) if address.path is not None else None
|
|
314
|
+
PendingSave.originals[address] = codec.load(
|
|
315
|
+
address=address, **{**kwargs, "source_text": disk_text})
|
|
316
|
+
imgui.text("No original data to compare against for address: {}".format(address))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def paint_breadcrumbs(draw_state, path, click=None, crumb_height=24.0, left_pad=6.0,
|
|
320
|
+
folder_bg_boost=-0.12, text_mix=0.5, width=None, file_metadata=None):
|
|
321
|
+
"""The path strip: every segment of `path` a crumb on the window draw
|
|
322
|
+
list at the cursor, the last one bright (the current directory / the
|
|
323
|
+
file), the rest dim, a painted folder wearing its file-meta tint, the
|
|
324
|
+
hovered one a faint wash. Advances the cursor by the strip's height.
|
|
325
|
+
`click` is the caller's injected left-click position (screen space, or
|
|
326
|
+
None): returns the clicked segment's Path when it is not the last one,
|
|
327
|
+
else None. Shared by the file listing (`draw_file_listing`, the strip
|
|
328
|
+
over the rows) and `draw_breadcrumbs` (the strip on its own)."""
|
|
329
|
+
from meltygui.files.fast_file_explorer import row_tint_bg
|
|
330
|
+
from meltygui.files.fast_file_explorer import tinted_text
|
|
331
|
+
from meltygui.models.file_meta import FileMeta
|
|
332
|
+
|
|
333
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
334
|
+
crumb_separator = " / "
|
|
335
|
+
text_rgba = (0.92, 0.92, 0.92, 1.0)
|
|
336
|
+
text_col = pack_color(*text_rgba)
|
|
337
|
+
dim_col = pack_color(0.6, 0.63, 0.68, 1.0)
|
|
338
|
+
crumb_hover_col = pack_color(1.0, 1.0, 1.0, 0.12)
|
|
339
|
+
|
|
340
|
+
px = Melty.px
|
|
341
|
+
pad, crumb_h = px(left_pad), px(crumb_height)
|
|
342
|
+
parts = Path(path).parts
|
|
343
|
+
meta = file_metadata
|
|
344
|
+
row_bg = row_tint_bg()
|
|
345
|
+
draw_list = imgui.get_window_draw_list()
|
|
346
|
+
content_w = width if width is not None else (draw_state.content_width or draw_state.width or 240)
|
|
347
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
348
|
+
hover_ok = draw_state._bounding_hovered
|
|
349
|
+
x0, y0 = imgui.get_cursor_screen_pos()
|
|
350
|
+
crumbs = [] # (x_left, x_right, target Path)
|
|
351
|
+
cx = x0 + pad
|
|
352
|
+
crumb_y = y0 + (crumb_h - imgui.get_font_size()) * 0.5
|
|
353
|
+
for i, part in enumerate(parts):
|
|
354
|
+
label = part if part != os.sep else os.sep
|
|
355
|
+
label_w = imgui.calc_text_size(label).x
|
|
356
|
+
target = Path(*parts[:i + 1])
|
|
357
|
+
crumbs.append((cx - px(3), cx + label_w + px(3), target))
|
|
358
|
+
hovered = hover_ok and cx - px(3) <= mouse_x < cx + label_w + px(3) and y0 <= mouse_y < y0 + crumb_h
|
|
359
|
+
crumb_tint = FileMeta.painted_tint(meta.get(str(target))) if meta is not None else None
|
|
360
|
+
if crumb_tint:
|
|
361
|
+
draw_list.add_rect_filled(cx - px(3), y0 + px(2), cx + label_w + px(3), y0 + crumb_h - px(2),
|
|
362
|
+
row_bg(crumb_tint, folder_bg_boost or 0.0), rounding=px(3))
|
|
363
|
+
if hovered:
|
|
364
|
+
draw_list.add_rect_filled(cx - px(3), y0 + px(2), cx + label_w + px(3), y0 + crumb_h - px(2),
|
|
365
|
+
crumb_hover_col, rounding=px(3))
|
|
366
|
+
last = i == len(parts) - 1
|
|
367
|
+
crumb_col = (tinted_text(text_rgba, crumb_tint, text_mix) if crumb_tint
|
|
368
|
+
else text_col if last else dim_col)
|
|
369
|
+
draw_list.add_text(cx, crumb_y, crumb_col, label)
|
|
370
|
+
cx += label_w
|
|
371
|
+
if not last and part == os.sep:
|
|
372
|
+
cx += px(8) # Keep the root chip separate from the first folder.
|
|
373
|
+
elif not last:
|
|
374
|
+
draw_list.add_text(cx, crumb_y, dim_col, crumb_separator)
|
|
375
|
+
cx += imgui.calc_text_size(crumb_separator).x
|
|
376
|
+
imgui.dummy(content_w, crumb_h)
|
|
377
|
+
if click is not None and crumbs and y0 <= click[1] < y0 + crumb_h:
|
|
378
|
+
for left, right, target in crumbs[:-1]:
|
|
379
|
+
if left <= click[0] < right:
|
|
380
|
+
return target
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@render_func(tint=(0.32, 0.42, 0.54), selectable=False, disable_scroll=True,
|
|
385
|
+
show_add_delete=False, is_tree=False, show_bg=False, shadow=False)
|
|
386
|
+
def draw_breadcrumbs(input_value: str, draw_state, left_mouse_clicked=False,
|
|
387
|
+
crumb_height=24.0, left_pad=6.0, folder_bg_boost=-0.12,
|
|
388
|
+
text_mix=0.5, file_metadata=None, **kwargs):
|
|
389
|
+
"""The file browser's path strip on its own (`paint_breadcrumbs`) for a
|
|
390
|
+
host that shows one path — the code editor draws it along the top of
|
|
391
|
+
the selected file's column (``show_breadcrumbs=True``), the mirror of
|
|
392
|
+
the tab bar along its bottom. `input_value` is the path (a directory or
|
|
393
|
+
a file; a file's name is the last, bright crumb). A click on any other
|
|
394
|
+
crumb returns ``(True, directory)`` once, the host deciding what a
|
|
395
|
+
directory pick means; otherwise ``(False, input_value)``."""
|
|
396
|
+
if not input_value:
|
|
397
|
+
return False, input_value
|
|
398
|
+
click = ((left_mouse_clicked.x, left_mouse_clicked.y)
|
|
399
|
+
if (left_mouse_clicked and hasattr(left_mouse_clicked, "x")) else None)
|
|
400
|
+
target = paint_breadcrumbs(draw_state, input_value, click=click,
|
|
401
|
+
crumb_height=crumb_height, left_pad=left_pad,
|
|
402
|
+
folder_bg_boost=folder_bg_boost, text_mix=text_mix,
|
|
403
|
+
file_metadata=file_metadata)
|
|
404
|
+
if target is not None:
|
|
405
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
406
|
+
request_render()
|
|
407
|
+
return True, str(target)
|
|
408
|
+
return False, input_value
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@render_func(tint=(0.32, 0.42, 0.54), selectable=False, disable_scroll=False,
|
|
412
|
+
show_add_delete=False, is_tree=False, show_bg=False, shadow=False)
|
|
413
|
+
def draw_file_listing(input_value: str, draw_state, explorer_state: FileExplorerState, file_metadata=None,
|
|
414
|
+
left_mouse_down=False, left_mouse_double_clicked=False,
|
|
415
|
+
right_mouse_down=False,
|
|
416
|
+
ctrl_up_key_pressed=False, up_key_pressed=False, down_key_pressed=False,
|
|
417
|
+
enter_key_pressed=False, escape_key_pressed=False,
|
|
418
|
+
row_height=20.0, left_pad=6.0, glyph_width=18.0, crumb_height=24.0,
|
|
419
|
+
show_tint_chips=True, chip_size=17.0, default_tint=(0.32, 0.42, 0.54, 1.0),
|
|
420
|
+
select_boost=0.22, plain_select_boost=0.06, select_shadow=2.0,
|
|
421
|
+
select_rounding=3.0, chip_mix=0.55, hover_boost=0.06, hover_alpha=0.05, text_mix=0.5, icon_mix=0.9,
|
|
422
|
+
folder_bg_boost=-0.12, folder_bg_rounding=0.0, drag_rows=True, menu_target=None,
|
|
423
|
+
type_to_search=True, search_tint=(1.0, 0.82, 0.3), search_dim=0.45,
|
|
424
|
+
search_flash_frames=36, show_crumbs=True, show_hidden=None,
|
|
425
|
+
**kwargs):
|
|
426
|
+
"""The path strip + rows of one directory (see the module docstring).
|
|
427
|
+
`show_crumbs=False` leaves the strip out (a host drawing the crumbs in
|
|
428
|
+
its own toolbar, where they stay put while the rows scroll).
|
|
429
|
+
Returns ``(True, path)`` on navigation / a file double-click, else
|
|
430
|
+
``(False, input_value)``. `show_tint_chips` puts the tint chip / brush
|
|
431
|
+
before each row's icon; `default_tint` is what the brush stamps. The
|
|
432
|
+
selected row is its own tint brightened by `select_boost` (an unpainted
|
|
433
|
+
row: `default_tint` by the smaller `plain_select_boost`, so it stays
|
|
434
|
+
close to the background), lifted off the list by an add_shadow of
|
|
435
|
+
`select_shadow` depth (0 disables it). `chip_mix` pulls the tint chip's
|
|
436
|
+
colour toward its row background (0 = the raw tint). Hover is the same
|
|
437
|
+
tint brightened by `hover_boost` on the selected row; any other hovered
|
|
438
|
+
row gets only a faint white wash of `hover_alpha`. A painted
|
|
439
|
+
row wears its tint on its text (mixed `text_mix` toward the tint) and
|
|
440
|
+
its icon (`icon_mix`, stronger), not as a row background. `type_to_search`
|
|
441
|
+
is the keyboard search of the module docstring: `search_tint` colours
|
|
442
|
+
the matched letters and the pill, the non-matching rows' text fades to
|
|
443
|
+
`search_dim` of its alpha while there are matches, and the flash on the
|
|
444
|
+
row a keystroke lands on fades over `search_flash_frames` frames."""
|
|
445
|
+
from meltygui.model.file_model import _dir_mtime_ns
|
|
446
|
+
from meltygui.files.fast_file_explorer import _scroll_row_into_view
|
|
447
|
+
from meltygui.model.file_metadata_model import apply_row_drop
|
|
448
|
+
from meltygui.files.fast_file_explorer import chip_swatch
|
|
449
|
+
from meltygui.files.fast_file_explorer import claim_keyboard
|
|
450
|
+
from meltygui.model.file_model import list_directory
|
|
451
|
+
from meltygui.model.file_metadata_model import ordered_rows
|
|
452
|
+
from meltygui.files.fast_file_explorer import row_icon
|
|
453
|
+
from meltygui.files.fast_file_explorer import row_tint_bg
|
|
454
|
+
from meltygui.files.fast_file_explorer import search_hits
|
|
455
|
+
from meltygui.files.fast_file_explorer import search_keys
|
|
456
|
+
from meltygui.files.fast_file_explorer import search_typed
|
|
457
|
+
from meltygui.model.file_metadata_model import set_row_order
|
|
458
|
+
from meltygui.files.fast_file_explorer import tint_control
|
|
459
|
+
from meltygui.model.file_metadata_model import set_row_tint
|
|
460
|
+
from meltygui.files.fast_file_explorer import tinted_text
|
|
461
|
+
from meltygui.core.files.file_explorer_core import watch_directory
|
|
462
|
+
from meltygui.models.file_meta import FileMeta
|
|
463
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
464
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
465
|
+
from meltygui.core.cache.tile_cache import clear_glows
|
|
466
|
+
from meltygui.core.input.drag_drop_core import DragDrop
|
|
467
|
+
|
|
468
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
469
|
+
folder_icon = f""
|
|
470
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
471
|
+
file_icon = f""
|
|
472
|
+
text_rgba = (0.92, 0.92, 0.92, 1.0)
|
|
473
|
+
text_col = pack_color(*text_rgba)
|
|
474
|
+
dim_col = pack_color(0.6, 0.63, 0.68, 1.0)
|
|
475
|
+
folder_rgba = (0.78, 0.84, 0.92, 1.0)
|
|
476
|
+
folder_col = pack_color(*folder_rgba)
|
|
477
|
+
search_wash = pack_color(search_tint[0], search_tint[1], search_tint[2], 0.30)
|
|
478
|
+
search_col = pack_color(search_tint[0], search_tint[1], search_tint[2], 1.0)
|
|
479
|
+
no_match_col = pack_color(0.95, 0.55, 0.5, 1.0)
|
|
480
|
+
hover_wash = pack_color(1.0, 1.0, 1.0, hover_alpha)
|
|
481
|
+
|
|
482
|
+
state = explorer_state
|
|
483
|
+
# The selected row's add_shadow is RETAINED under this draw_state until
|
|
484
|
+
# the caller opens its group again: a selection that vanishes (the file
|
|
485
|
+
# trashed, Esc, a new directory) would otherwise keep its shadow.
|
|
486
|
+
clear_glows(draw_state)
|
|
487
|
+
px = Melty.px
|
|
488
|
+
row_h, pad, glyph_w, crumb_h = px(row_height), px(left_pad), px(glyph_width), px(crumb_height)
|
|
489
|
+
chip = px(chip_size) if show_tint_chips else 0.0
|
|
490
|
+
# The tint control leads the row; icon & name shift right past it.
|
|
491
|
+
chip_x = 0.0
|
|
492
|
+
text_x = pad + (chip + px(6) if show_tint_chips else 0.0)
|
|
493
|
+
directory = Path(input_value if input_value else Path.home()).expanduser()
|
|
494
|
+
dir_key = str(directory)
|
|
495
|
+
draw_list = imgui.get_window_draw_list()
|
|
496
|
+
content_w = draw_state.content_width or (draw_state.width or 240)
|
|
497
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
498
|
+
hover_ok = draw_state._bounding_hovered
|
|
499
|
+
click = ((left_mouse_down.x, left_mouse_down.y)
|
|
500
|
+
if (left_mouse_down and hasattr(left_mouse_down, "x")) else None)
|
|
501
|
+
double_click = ((left_mouse_double_clicked.x, left_mouse_double_clicked.y)
|
|
502
|
+
if (left_mouse_double_clicked and hasattr(left_mouse_double_clicked, "x")) else None)
|
|
503
|
+
right_press = ((right_mouse_down.x, right_mouse_down.y)
|
|
504
|
+
if (right_mouse_down and hasattr(right_mouse_down, "x")) else None)
|
|
505
|
+
meta = file_metadata
|
|
506
|
+
row_bg = row_tint_bg()
|
|
507
|
+
|
|
508
|
+
def navigate(target):
|
|
509
|
+
"""Leave `dir_key` for `target`: remember where this listing was
|
|
510
|
+
scrolled so a return lands there, and hand the path to the caller."""
|
|
511
|
+
state.scroll_by_dir[dir_key] = draw_state.scroll_offset[1]
|
|
512
|
+
request_render()
|
|
513
|
+
return True, str(target)
|
|
514
|
+
|
|
515
|
+
# ── a new directory: restore the scroll, drop the selection, move the watch ──
|
|
516
|
+
if state._last_dir != dir_key:
|
|
517
|
+
state._last_dir = dir_key
|
|
518
|
+
state.selected = None
|
|
519
|
+
state._search, state._search_for = "", None
|
|
520
|
+
draw_state.scroll_offset = (0.0, state.scroll_by_dir.get(dir_key, 0.0))
|
|
521
|
+
draw_state.invalidate()
|
|
522
|
+
watch_directory(draw_state, state, dir_key)
|
|
523
|
+
|
|
524
|
+
# ── listing, memoized by the directory's mtime (renames / new files bump it) ──
|
|
525
|
+
mtime = _dir_mtime_ns(directory)
|
|
526
|
+
listing = state._listing
|
|
527
|
+
if show_hidden is not None:
|
|
528
|
+
state.show_hidden = show_hidden
|
|
529
|
+
if listing is None or listing[:3] != (dir_key, mtime, state.show_hidden):
|
|
530
|
+
listing = state._listing = (dir_key, mtime, state.show_hidden,
|
|
531
|
+
list_directory(directory, state.show_hidden))
|
|
532
|
+
rows = ordered_rows(listing[3], meta)
|
|
533
|
+
|
|
534
|
+
# ── the path strip: every segment a crumb; click = jump there ──
|
|
535
|
+
if show_crumbs:
|
|
536
|
+
target = paint_breadcrumbs(draw_state, directory, click=click,
|
|
537
|
+
crumb_height=crumb_height, left_pad=left_pad,
|
|
538
|
+
file_metadata=file_metadata,
|
|
539
|
+
folder_bg_boost=folder_bg_boost, text_mix=text_mix)
|
|
540
|
+
if target is not None:
|
|
541
|
+
return navigate(target)
|
|
542
|
+
|
|
543
|
+
# ── content height: rows + the top inset (the fast_dock rule) ──
|
|
544
|
+
rows_x, rows_y = imgui.get_cursor_screen_pos()
|
|
545
|
+
# ── a painted directory: a tint wash below the breadcrumb strip ──
|
|
546
|
+
dir_tint = FileMeta.painted_tint(meta.get(dir_key)) if meta is not None else None
|
|
547
|
+
if dir_tint and folder_bg_boost is not None:
|
|
548
|
+
wash = getattr(draw_state, "abs_clip_rect", None)
|
|
549
|
+
if wash is None:
|
|
550
|
+
wash = (draw_state.abs_left, draw_state.abs_top,
|
|
551
|
+
draw_state.abs_left + (draw_state.width or 0),
|
|
552
|
+
draw_state.abs_top + (draw_state.height or 0))
|
|
553
|
+
wash_top = max(wash[1], rows_y)
|
|
554
|
+
if wash_top < wash[3]:
|
|
555
|
+
draw_list.add_rect_filled(wash[0], wash_top, wash[2], wash[3],
|
|
556
|
+
row_bg(dir_tint, folder_bg_boost), rounding=px(folder_bg_rounding))
|
|
557
|
+
|
|
558
|
+
top_inset = (rows_y + draw_state.scroll_offset[1]) - draw_state.abs_top
|
|
559
|
+
imgui.dummy(content_w, max(1.0, len(rows) * row_h + max(0.0, top_inset)))
|
|
560
|
+
|
|
561
|
+
# ── input on rows ──
|
|
562
|
+
# The tint control's widget (chip / brush) at the row's left edge is
|
|
563
|
+
# the chip's own click, never the row's - a double-click there must
|
|
564
|
+
# not navigate.
|
|
565
|
+
chip_x = rows_x + pad
|
|
566
|
+
clip = getattr(draw_state, "abs_clip_rect", None)
|
|
567
|
+
|
|
568
|
+
drag_left = chip_x + chip + px(2) if show_tint_chips else rows_x
|
|
569
|
+
|
|
570
|
+
def row_at(point, chips=False):
|
|
571
|
+
if point is None or not (rows_x <= point[0] <= rows_x + content_w):
|
|
572
|
+
return None
|
|
573
|
+
if not chips and show_tint_chips and point[0] < drag_left:
|
|
574
|
+
return None
|
|
575
|
+
index = int((point[1] - rows_y) // row_h)
|
|
576
|
+
return index if 0 <= index < len(rows) else None
|
|
577
|
+
|
|
578
|
+
selected_index = None
|
|
579
|
+
if state.selected is not None:
|
|
580
|
+
for i, (path, _is_dir) in enumerate(rows):
|
|
581
|
+
if str(path) == state.selected:
|
|
582
|
+
selected_index = i
|
|
583
|
+
break
|
|
584
|
+
|
|
585
|
+
# ── the keyboard: the type-to-search queue when this listing owns it,
|
|
586
|
+
# else the hover-routed key params (the find box has the keyboard, say) ──
|
|
587
|
+
owns_keys = type_to_search and claim_keyboard(draw_state)
|
|
588
|
+
query = state._search if type_to_search else ""
|
|
589
|
+
step = (1 if down_key_pressed else 0) - (1 if up_key_pressed else 0)
|
|
590
|
+
if owns_keys:
|
|
591
|
+
keys = search_keys()
|
|
592
|
+
query, step, enter_key_pressed, ctrl_up_key_pressed, escape_key_pressed = \
|
|
593
|
+
search_typed(query, keys)
|
|
594
|
+
if keys:
|
|
595
|
+
draw_state.invalidate()
|
|
596
|
+
request_render()
|
|
597
|
+
if query != state._search:
|
|
598
|
+
state._search = query
|
|
599
|
+
rows_top = rows_y - draw_state.abs_top + draw_state.scroll_offset[1]
|
|
600
|
+
view_rect = clip if clip is not None else (
|
|
601
|
+
draw_state.abs_left, draw_state.abs_top,
|
|
602
|
+
draw_state.abs_left + (draw_state.width or 0), draw_state.abs_top + (draw_state.height or 0))
|
|
603
|
+
|
|
604
|
+
def flash_row(index):
|
|
605
|
+
"""Melty.emphasize on row `index`: a fire-and-forget flash whose rect
|
|
606
|
+
follows the scroll, scissored to the listing (keyed by the row's
|
|
607
|
+
path, so a new target starts a fresh fade)."""
|
|
608
|
+
top = rows_top + index * row_h
|
|
609
|
+
|
|
610
|
+
def rect(ds=draw_state, top=top, h=row_h, x0=rows_x, w=content_w):
|
|
611
|
+
y = ds.abs_top + top - ds.scroll_offset[1]
|
|
612
|
+
return (x0, y, x0 + w, y + h)
|
|
613
|
+
|
|
614
|
+
def clip_rect(ds=draw_state, fallback=view_rect):
|
|
615
|
+
return getattr(ds, "abs_clip_rect", None) or fallback
|
|
616
|
+
|
|
617
|
+
Melty.emphasize(f"explorer search {draw_state._tile_id} {rows[index][0]}", rect,
|
|
618
|
+
clip=clip_rect, rounding=px(select_rounding),
|
|
619
|
+
fade_frames=search_flash_frames)
|
|
620
|
+
|
|
621
|
+
def jump_to_row(index):
|
|
622
|
+
"""Select row `index` and bring it into view; the flash only when the
|
|
623
|
+
landing row CHANGES (a keystroke that keeps the same best match, or
|
|
624
|
+
the same step target, is quiet)."""
|
|
625
|
+
moved = index != selected_index
|
|
626
|
+
state.selected = str(rows[index][0])
|
|
627
|
+
_scroll_row_into_view(draw_state, index, row_h, rows_top, centre=True,
|
|
628
|
+
content_h=len(rows) * row_h + max(0.0, top_inset))
|
|
629
|
+
if moved:
|
|
630
|
+
flash_row(index)
|
|
631
|
+
request_render()
|
|
632
|
+
return index
|
|
633
|
+
|
|
634
|
+
hit = row_at(double_click)
|
|
635
|
+
if hit is not None:
|
|
636
|
+
return navigate(rows[hit][0])
|
|
637
|
+
hit = row_at(click)
|
|
638
|
+
if hit is not None:
|
|
639
|
+
state.selected = str(rows[hit][0])
|
|
640
|
+
selected_index = hit
|
|
641
|
+
request_render()
|
|
642
|
+
# A right-press selects the row under it (the chip column included): the
|
|
643
|
+
# wrapper opens the context menu on the release, on that row.
|
|
644
|
+
hit = row_at(right_press, chips=True)
|
|
645
|
+
if hit is not None:
|
|
646
|
+
state.selected = str(rows[hit][0])
|
|
647
|
+
selected_index = hit
|
|
648
|
+
draw_state.invalidate()
|
|
649
|
+
request_render()
|
|
650
|
+
elif right_press is not None and state.selected is not None:
|
|
651
|
+
# Empty space: the menu acts on the dir, not a stale selection.
|
|
652
|
+
state.selected = None
|
|
653
|
+
selected_index = None
|
|
654
|
+
draw_state.invalidate()
|
|
655
|
+
request_render()
|
|
656
|
+
if menu_target is not None:
|
|
657
|
+
menu_target["path"] = state.selected if selected_index is not None else dir_key
|
|
658
|
+
if ctrl_up_key_pressed and directory.parent != directory:
|
|
659
|
+
return navigate(directory.parent)
|
|
660
|
+
if enter_key_pressed and selected_index is not None:
|
|
661
|
+
return navigate(rows[selected_index][0])
|
|
662
|
+
if escape_key_pressed and state.selected is not None:
|
|
663
|
+
state.selected = None
|
|
664
|
+
draw_state.invalidate()
|
|
665
|
+
request_render()
|
|
666
|
+
|
|
667
|
+
# ── the search: rank the rows, land on the best, step through the rest ──
|
|
668
|
+
# A query change (or a selection that left no matches: a click in the
|
|
669
|
+
# directory changed under the watch) lands on the best match; Up / Down
|
|
670
|
+
# / Tab walk the matches in listing order, wrapping. Without a query the
|
|
671
|
+
# steps walk the whole listing as before. No match: the selection stays
|
|
672
|
+
# where it was and the listing says so.
|
|
673
|
+
hits, best = search_hits(rows, query) if query else ([], None)
|
|
674
|
+
hit_rows = {index: spans for index, _rank, spans in hits}
|
|
675
|
+
if query and hits:
|
|
676
|
+
positions = [index for index, _rank, _spans in hits]
|
|
677
|
+
if state._search_for != query or selected_index not in hit_rows:
|
|
678
|
+
selected_index = jump_to_row(positions[best])
|
|
679
|
+
elif step:
|
|
680
|
+
at = positions.index(selected_index)
|
|
681
|
+
selected_index = jump_to_row(positions[(at + step) % len(positions)])
|
|
682
|
+
elif step and rows and not query:
|
|
683
|
+
selected_index = (0 if selected_index is None and step > 0
|
|
684
|
+
else len(rows) - 1 if selected_index is None
|
|
685
|
+
else max(0, min(len(rows) - 1, selected_index + step)))
|
|
686
|
+
state.selected = str(rows[selected_index][0])
|
|
687
|
+
_scroll_row_into_view(draw_state, selected_index, row_h, rows_top)
|
|
688
|
+
request_render()
|
|
689
|
+
state._search_for = query if query else None
|
|
690
|
+
if query and not hits and step:
|
|
691
|
+
request_render()
|
|
692
|
+
dimmed = bool(query and hits)
|
|
693
|
+
|
|
694
|
+
# ── rows: viewport-culled, straight to the draw list ──
|
|
695
|
+
text_y_pad = (row_h - imgui.get_font_size()) * 0.5
|
|
696
|
+
ghost_alpha = 0.9
|
|
697
|
+
drag_keys = [] # the visible rows' paths, in on_drag call order
|
|
698
|
+
first_visible = None # index into `rows` of drag_keys[0]
|
|
699
|
+
for i, (path, is_dir) in enumerate(rows):
|
|
700
|
+
ry0 = rows_y + i * row_h
|
|
701
|
+
ry1 = ry0 + row_h
|
|
702
|
+
if clip is not None and (ry1 < clip[1] or ry0 > clip[3]):
|
|
703
|
+
continue
|
|
704
|
+
key = str(path)
|
|
705
|
+
entry = meta.get(key) if meta is not None else None
|
|
706
|
+
tint = FileMeta.painted_tint(entry)
|
|
707
|
+
icon = row_icon(path, is_dir, entry, folder_icon, file_icon)
|
|
708
|
+
spans = hit_rows.get(i) if dimmed else None
|
|
709
|
+
name_rgba, glyph_rgba = (folder_rgba if is_dir else text_rgba), text_rgba
|
|
710
|
+
if dimmed and spans is None:
|
|
711
|
+
name_rgba = name_rgba[:3] + (name_rgba[3] * search_dim,)
|
|
712
|
+
glyph_rgba = glyph_rgba[:3] + (glyph_rgba[3] * search_dim,)
|
|
713
|
+
if tint:
|
|
714
|
+
name_col = tinted_text(name_rgba, tint, text_mix)
|
|
715
|
+
icon_col = tinted_text(glyph_rgba, tint, icon_mix)
|
|
716
|
+
elif dimmed and spans is None:
|
|
717
|
+
name_col = pack_color(*name_rgba)
|
|
718
|
+
icon_col = pack_color(*glyph_rgba)
|
|
719
|
+
else:
|
|
720
|
+
name_col = icon_col = folder_col if is_dir else text_col
|
|
721
|
+
if drag_rows:
|
|
722
|
+
# The tab bar's immediate-mode DragDrop: the row (past the chip
|
|
723
|
+
# column) is its own drag handle. While THIS row is the drag,
|
|
724
|
+
# on_drag has parked the cursor at the ghost: paint the row
|
|
725
|
+
# there on the overlay, but leave the inline row alone
|
|
726
|
+
# (DragDrop treats it as the home / drop target).
|
|
727
|
+
if first_visible is None:
|
|
728
|
+
first_visible = i
|
|
729
|
+
drag_keys.append(path)
|
|
730
|
+
drag = DragDrop.on_drag((drag_left, ry0, rows_x + content_w, ry1), key=key,
|
|
731
|
+
draw_state=draw_state)
|
|
732
|
+
if drag:
|
|
733
|
+
ghost = drag.draw_list
|
|
734
|
+
ghost.add_rect_filled(drag.x, drag.y, drag.x + drag.w, drag.y + drag.h,
|
|
735
|
+
pack_color(*row_bg.rgb(tint or default_tint, select_boost),
|
|
736
|
+
ghost_alpha),
|
|
737
|
+
rounding=px(select_rounding))
|
|
738
|
+
ghost.add_text(drag.x + px(4), drag.y + text_y_pad, icon_col, icon)
|
|
739
|
+
ghost.add_text(drag.x + px(4) + glyph_w, drag.y + text_y_pad, name_col, path.name)
|
|
740
|
+
DragDrop.end_drag()
|
|
741
|
+
continue
|
|
742
|
+
row_hovered = hover_ok and rows_x <= mouse_x <= rows_x + content_w and ry0 <= mouse_y < ry1
|
|
743
|
+
# Selection / hover are brighter steps of the row's own tint
|
|
744
|
+
# (the default tint when unpainted); they stack.
|
|
745
|
+
boost = (select_boost if tint else plain_select_boost) if i == selected_index else 0.0
|
|
746
|
+
if i == selected_index and select_shadow:
|
|
747
|
+
add_shadow((rows_x, ry0, content_w, row_h), offset=select_shadow,
|
|
748
|
+
corner_radius=px(select_rounding), clip=clip, draw_state=draw_state)
|
|
749
|
+
if boost:
|
|
750
|
+
draw_list.add_rect_filled(rows_x, ry0, rows_x + content_w, ry1,
|
|
751
|
+
row_bg(tint or default_tint,
|
|
752
|
+
boost + (hover_boost if row_hovered else 0.0)),
|
|
753
|
+
rounding=px(select_rounding))
|
|
754
|
+
elif row_hovered:
|
|
755
|
+
draw_list.add_rect_filled(rows_x, ry0, rows_x + content_w, ry1, hover_wash,
|
|
756
|
+
rounding=px(select_rounding))
|
|
757
|
+
draw_list.add_text(rows_x + text_x, ry0 + text_y_pad, icon_col, icon)
|
|
758
|
+
name_x = rows_x + text_x + glyph_w
|
|
759
|
+
if spans:
|
|
760
|
+
# The matched letters: a wash of the search tint over them.
|
|
761
|
+
name = path.name
|
|
762
|
+
for start, end in spans:
|
|
763
|
+
sx0 = name_x + imgui.calc_text_size(name[:start]).x
|
|
764
|
+
sx1 = sx0 + imgui.calc_text_size(name[start:end]).x
|
|
765
|
+
draw_list.add_rect_filled(sx0 - px(1), ry0 + px(2), sx1 + px(1), ry1 - px(2),
|
|
766
|
+
search_wash, rounding=px(2))
|
|
767
|
+
draw_list.add_text(name_x, ry0 + text_y_pad, name_col, path.name)
|
|
768
|
+
if show_tint_chips:
|
|
769
|
+
swatch = None
|
|
770
|
+
if tint:
|
|
771
|
+
swatch = chip_swatch(tint, row_bg.rgb(tint, boost), chip_mix)
|
|
772
|
+
tint_control(draw_state, key, tint, chip_x, ry0 + (row_h - chip) * 0.5, chip,
|
|
773
|
+
ry0 + text_y_pad, row_hovered, default_tint, swatch=swatch,
|
|
774
|
+
setter=lambda value, path=key: set_row_tint(meta, path, value),
|
|
775
|
+
show_brush=i == selected_index)
|
|
776
|
+
|
|
777
|
+
# ── the search pill: the query and "n of m", bottom right of the view ──
|
|
778
|
+
if query:
|
|
779
|
+
icon_search = "\uf002"
|
|
780
|
+
count = (f"{positions.index(selected_index) + 1} of {len(hits)}"
|
|
781
|
+
if hits and selected_index in hit_rows else "no match")
|
|
782
|
+
pill_h = px(26)
|
|
783
|
+
pad_x, gap = px(11), px(12)
|
|
784
|
+
query_w = imgui.calc_text_size(query).x
|
|
785
|
+
icon_w = imgui.calc_text_size(icon_search).x
|
|
786
|
+
count_w = imgui.calc_text_size(count).x
|
|
787
|
+
pill_w = pad_x + icon_w + px(8) + query_w + gap + count_w + pad_x
|
|
788
|
+
px1 = view_rect[2] - px(14)
|
|
789
|
+
py1 = view_rect[3] - px(12)
|
|
790
|
+
px0 = max(view_rect[0] + px(6), px1 - pill_w)
|
|
791
|
+
py0 = py1 - pill_h
|
|
792
|
+
draw_list.add_rect_filled(px0 + px(1), py0 + px(2), px1 + px(1), py1 + px(2),
|
|
793
|
+
pack_color(0.0, 0.0, 0.0, 0.35), rounding=pill_h * 0.5)
|
|
794
|
+
draw_list.add_rect_filled(px0, py0, px1, py1,
|
|
795
|
+
pack_color(*row_bg.rgb(default_tint, -0.02), 0.97),
|
|
796
|
+
rounding=pill_h * 0.5)
|
|
797
|
+
draw_list.add_rect(px0, py0, px1, py1, pack_color(1.0, 1.0, 1.0, 0.14),
|
|
798
|
+
rounding=pill_h * 0.5)
|
|
799
|
+
pill_ty = py0 + (pill_h - imgui.get_font_size()) * 0.5
|
|
800
|
+
draw_list.add_text(px0 + pad_x, pill_ty, search_col, icon_search)
|
|
801
|
+
draw_list.add_text(px0 + pad_x + icon_w + px(8), pill_ty, text_col, query)
|
|
802
|
+
draw_list.add_text(px1 - pad_x - count_w, pill_ty,
|
|
803
|
+
dim_col if hits else no_match_col, count)
|
|
804
|
+
|
|
805
|
+
# ── close the drag body: paint the between-row slots, apply a drop ──
|
|
806
|
+
# A reorder lands in on_drag call order = the visible rows, a contiguous
|
|
807
|
+
# slice of `rows`; splice the new slice in and stamp the whole
|
|
808
|
+
# directory's order (folders and files together - the stamps are the order
|
|
809
|
+
# from now on). Cross-collection kinds ("insert" / "remove") are ignored:
|
|
810
|
+
# rows only reorder here.
|
|
811
|
+
if drag_rows:
|
|
812
|
+
drop = DragDrop.on_drop(draw_state=draw_state)
|
|
813
|
+
order = apply_row_drop(rows, drag_keys, first_visible, drop)
|
|
814
|
+
if order is not None:
|
|
815
|
+
set_row_order(meta, order)
|
|
816
|
+
draw_state.invalidate()
|
|
817
|
+
request_render()
|
|
818
|
+
|
|
819
|
+
return False, input_value
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@render_func(tint=(0.32, 0.42, 0.54), selectable=False, disable_scroll=True, use_cache=True,
|
|
823
|
+
show_add_delete=False, is_tree=False, show_bg=False, shadow=False)
|
|
824
|
+
def draw_shortcuts(input_value: str, draw_state, file_metadata=None, left_mouse_clicked=False,
|
|
825
|
+
shortcut_state: ShortcutState = None, row_height=22.0,
|
|
826
|
+
show_tint_chips=True, chip_size=17.0, default_tint=(0.32, 0.42, 0.54, 1.0),
|
|
827
|
+
show_projects=True, **kwargs):
|
|
828
|
+
"""The shortcuts column on its own: home, the XDG user directories and
|
|
829
|
+
the root as draw-list rows, then a **Projects** section — every folder
|
|
830
|
+
marked with meltygui.mark_project (the shared file-meta store's flag) —
|
|
831
|
+
and a click returns ``(True, path)`` once. `input_value` is the
|
|
832
|
+
directory the host shows (or None): the deepest shortcut / project
|
|
833
|
+
holding it is the CURRENT row (brighter, lifted by a shadow, its brush
|
|
834
|
+
showing). Every row leads with its folder's tint control (the same
|
|
835
|
+
file-meta tint the listings wear). Shortcuts drag to reorder, the order
|
|
836
|
+
persisting in `shortcut_state`; projects keep the store's path order.
|
|
837
|
+
The explorer draws this in its first cell; the code editor draws it as
|
|
838
|
+
a leading column (`show_shortcuts=True`), a pick selecting the project
|
|
839
|
+
in its injected `EditorProjectState`."""
|
|
840
|
+
from meltygui.core.runtime.extensions import source_folders as project_roots
|
|
841
|
+
from meltygui.files.fast_file_explorer import chip_swatch
|
|
842
|
+
from meltygui.files.fast_file_explorer import row_tint_bg
|
|
843
|
+
from meltygui.files.fast_file_explorer import shortcut_directories
|
|
844
|
+
from meltygui.files.fast_file_explorer import tint_control
|
|
845
|
+
from meltygui.model.file_metadata_model import set_row_tint
|
|
846
|
+
from meltygui.files.fast_file_explorer import tinted_text
|
|
847
|
+
from meltygui.models.file_meta import FileMeta
|
|
848
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
849
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
850
|
+
from meltygui.core.cache.tile_cache import clear_glows
|
|
851
|
+
from meltygui.core.input.drag_drop_core import DragDrop
|
|
852
|
+
|
|
853
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
854
|
+
folder_icon = f""
|
|
855
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
856
|
+
computer_icon = f""
|
|
857
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
858
|
+
left_pad = 8.0
|
|
859
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
860
|
+
glyph_width = 20.0
|
|
861
|
+
text_rgba = (0.85, 0.88, 0.92, 1.0)
|
|
862
|
+
text_col = pack_color(*text_rgba)
|
|
863
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
864
|
+
hover_boost = 0.06
|
|
865
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
866
|
+
hover_alpha = 0.05
|
|
867
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
868
|
+
text_mix = 0.5
|
|
869
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
870
|
+
icon_mix = 0.9
|
|
871
|
+
hover_wash = pack_color(1.0, 1.0, 1.0, hover_alpha)
|
|
872
|
+
|
|
873
|
+
px = Melty.px
|
|
874
|
+
clear_glows(draw_state) # the current row's retained shadow
|
|
875
|
+
directory = Path(input_value).expanduser() if input_value else None
|
|
876
|
+
draw_list = imgui.get_window_draw_list()
|
|
877
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
878
|
+
hover_ok = draw_state._bounding_hovered
|
|
879
|
+
click = ((left_mouse_clicked.x, left_mouse_clicked.y)
|
|
880
|
+
if (left_mouse_clicked and hasattr(left_mouse_clicked, "x")) else None)
|
|
881
|
+
chip = px(chip_size) if show_tint_chips else 0.0
|
|
882
|
+
text_x = px(left_pad) + (chip + px(6) if show_tint_chips else 0.0)
|
|
883
|
+
meta = file_metadata
|
|
884
|
+
row_bg = row_tint_bg()
|
|
885
|
+
x, y = imgui.get_cursor_screen_pos()
|
|
886
|
+
width = float(draw_state.content_width or draw_state.width or px(190))
|
|
887
|
+
row_h = px(row_height)
|
|
888
|
+
shortcuts = shortcut_directories()
|
|
889
|
+
ranks = {key: i for i, key in enumerate(shortcut_state.order)}
|
|
890
|
+
shortcuts.sort(key=lambda item: ranks.get(str(item[1]), len(ranks)))
|
|
891
|
+
# The second section: every folder marked as a project - an alphabetical
|
|
892
|
+
# list of the projects, the same rows in path order, no drag-reorder
|
|
893
|
+
# (the set is the store's, not this column's).
|
|
894
|
+
projects = ([(root.name or str(root), root) for root in project_roots()]
|
|
895
|
+
if show_projects else [])
|
|
896
|
+
# The shortcut / project that holds the current directory: the deepest one.
|
|
897
|
+
current = None
|
|
898
|
+
if directory is not None:
|
|
899
|
+
for label, path in shortcuts + projects:
|
|
900
|
+
if path == directory or path in directory.parents:
|
|
901
|
+
if current is None or len(path.parts) > len(current.parts):
|
|
902
|
+
current = path
|
|
903
|
+
text_y_pad = (row_h - imgui.get_font_size()) * 0.5
|
|
904
|
+
chip_x = x + px(left_pad)
|
|
905
|
+
# The tint control's column: the chip's own click, never the row's.
|
|
906
|
+
click_left = chip_x + chip + px(2) if show_tint_chips else x
|
|
907
|
+
section_gap = row_h * 0.5
|
|
908
|
+
|
|
909
|
+
def draw_rows(rows, y, draggable):
|
|
910
|
+
"""One section of rows from `y` down; returns the picked path."""
|
|
911
|
+
picked = None
|
|
912
|
+
for i, (label, path) in enumerate(rows):
|
|
913
|
+
ry0 = y + i * row_h
|
|
914
|
+
ry1 = ry0 + row_h
|
|
915
|
+
key = str(path)
|
|
916
|
+
tint = FileMeta.painted_tint(meta.get(key)) if meta is not None else None
|
|
917
|
+
icon = computer_icon if path == Path(os.sep) else folder_icon
|
|
918
|
+
label_col = tinted_text(text_rgba, tint, text_mix) if tint else text_col
|
|
919
|
+
icon_col = tinted_text(text_rgba, tint, icon_mix) if tint else text_col
|
|
920
|
+
drag = DragDrop.on_drag((click_left, ry0, x + width, ry1), key=key,
|
|
921
|
+
draw_state=draw_state) if draggable else None
|
|
922
|
+
if drag:
|
|
923
|
+
ghost = drag.draw_list
|
|
924
|
+
ghost.add_rect_filled(drag.x, drag.y, drag.x + drag.w, drag.y + drag.h,
|
|
925
|
+
row_bg(tint or default_tint, 0.22), rounding=px(4))
|
|
926
|
+
ghost.add_text(drag.x + px(4), drag.y + text_y_pad, icon_col, icon)
|
|
927
|
+
ghost.add_text(drag.x + px(4) + px(glyph_width), drag.y + text_y_pad,
|
|
928
|
+
label_col, label)
|
|
929
|
+
DragDrop.end_drag()
|
|
930
|
+
continue
|
|
931
|
+
row_hovered = hover_ok and x <= mouse_x < x + width and ry0 <= mouse_y < ry1
|
|
932
|
+
# The current row: its tint (default when unpainted), brighter,
|
|
933
|
+
# lifted above the column by a soft shadow; hover is a smaller
|
|
934
|
+
# brightening of the same tint.
|
|
935
|
+
boost = (0.22 if tint else 0.06) if path == current else 0.0
|
|
936
|
+
if path == current:
|
|
937
|
+
add_shadow((x, ry0, width, row_h), offset=2.0, corner_radius=px(4),
|
|
938
|
+
clip=getattr(draw_state, "abs_clip_rect", None), draw_state=draw_state)
|
|
939
|
+
if boost:
|
|
940
|
+
draw_list.add_rect_filled(x, ry0, x + width, ry1,
|
|
941
|
+
row_bg(tint or default_tint,
|
|
942
|
+
boost + (hover_boost if row_hovered else 0.0)),
|
|
943
|
+
rounding=px(4))
|
|
944
|
+
elif row_hovered:
|
|
945
|
+
draw_list.add_rect_filled(x, ry0, x + width, ry1, hover_wash, rounding=px(4))
|
|
946
|
+
draw_list.add_text(x + text_x, ry0 + text_y_pad, icon_col, icon)
|
|
947
|
+
draw_list.add_text(x + text_x + px(glyph_width), ry0 + text_y_pad, label_col, label)
|
|
948
|
+
if (click is not None and click_left <= click[0] < x + width and ry0 <= click[1] < ry1
|
|
949
|
+
and path != directory):
|
|
950
|
+
picked = path
|
|
951
|
+
if show_tint_chips:
|
|
952
|
+
swatch = None
|
|
953
|
+
if tint:
|
|
954
|
+
swatch = chip_swatch(tint, row_bg.rgb(tint, boost))
|
|
955
|
+
tint_control(draw_state, key, tint, chip_x, ry0 + (row_h - chip) * 0.5, chip,
|
|
956
|
+
ry0 + text_y_pad, row_hovered, default_tint, swatch=swatch,
|
|
957
|
+
setter=lambda value, path=key: set_row_tint(meta, path, value),
|
|
958
|
+
show_brush=path == current)
|
|
959
|
+
return picked
|
|
960
|
+
|
|
961
|
+
picked = draw_rows(shortcuts, y, draggable=True)
|
|
962
|
+
total_h = len(shortcuts) * row_h
|
|
963
|
+
if projects:
|
|
964
|
+
# A dim "Projects" heading half a row below the shortcuts.
|
|
965
|
+
head_y = y + total_h + section_gap
|
|
966
|
+
draw_list.add_text(x + text_x, head_y + text_y_pad,
|
|
967
|
+
imgui.get_color_u32_rgba(*text_rgba[:3], text_rgba[3] * 0.55),
|
|
968
|
+
"Projects")
|
|
969
|
+
picked = draw_rows(projects, head_y + row_h, draggable=False) or picked
|
|
970
|
+
total_h += section_gap + row_h + len(projects) * row_h
|
|
971
|
+
drop = DragDrop.on_drop(draw_state=draw_state)
|
|
972
|
+
if drop is not None and drop.kind == "reorder" and drop.apply(shortcuts):
|
|
973
|
+
shortcut_state.order = [str(path) for _label, path in shortcuts]
|
|
974
|
+
picked = None
|
|
975
|
+
imgui.dummy(width, total_h)
|
|
976
|
+
if picked is not None:
|
|
977
|
+
request_render()
|
|
978
|
+
return True, str(picked)
|
|
979
|
+
return False, input_value
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
@render_func(tint=(0.32, 0.42, 0.54), selectable=False, disable_scroll=True,
|
|
983
|
+
show_add_delete=False, is_tree=False, show_bg=False, shadow=False)
|
|
984
|
+
def draw_fast_file_explorer(input_value: str, draw_state, file_metadata=None, column_edges=None,
|
|
985
|
+
left_mouse_clicked=False, ctrl_up_key_pressed=False,
|
|
986
|
+
shortcuts_width=190.0, shortcut_row_height=22.0, column_gap=6.0,
|
|
987
|
+
show_tint_chips=True, chip_size=17.0, default_tint=(0.32, 0.42, 0.54, 1.0),
|
|
988
|
+
context_menu=None, drag_rows=True, folder_bg_boost=-0.12,
|
|
989
|
+
folder_bg_rounding=0.0, type_to_search=True, show_crumbs=True,
|
|
990
|
+
layout_out=None, show_hidden=None, **kwargs):
|
|
991
|
+
"""A ColumnLayout with two cells: `draw_shortcuts` (a click navigates)
|
|
992
|
+
and `draw_file_listing`, sharing one draggable edge
|
|
993
|
+
(`column_edges`, persisted by auto-state). Returns what the listing
|
|
994
|
+
returns; Ctrl+Up works from anywhere over the explorer. Shortcut rows
|
|
995
|
+
wear their directory's tint like the listing's, with the same leading
|
|
996
|
+
tint control (`show_tint_chips`, `chip_size`, `default_tint`).
|
|
997
|
+
`context_menu` = {label: callable(path)} is the listing's right-click
|
|
998
|
+
menu; each callable gets the path of the right-clicked row, or of the
|
|
999
|
+
directory (see the module docstring). `drag_rows` / `folder_bg_boost`
|
|
1000
|
+
go to the listing, as does `type_to_search` (the keyboard search of the
|
|
1001
|
+
module docstring; False leaves the keyboard alone), and `show_crumbs`
|
|
1002
|
+
(False drops the listing's path strip for a host that draws its own).
|
|
1003
|
+
`layout_out`, a dict, receives ``listing_left``: the absolute x where
|
|
1004
|
+
the listing column's content starts, so a host toolbar can line up with
|
|
1005
|
+
it (the frame's value lands after this call; a host drawing above the
|
|
1006
|
+
explorer reads last frame's). Shortcuts drag to reorder, with their own
|
|
1007
|
+
persisted order."""
|
|
1008
|
+
_trace_browser_size("file-explorer-size", draw_state,
|
|
1009
|
+
content_width=draw_state.content_width,
|
|
1010
|
+
size_change=getattr(draw_state, "size_change", None))
|
|
1011
|
+
from meltygui.files.fast_file_explorer import row_tint_bg
|
|
1012
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1013
|
+
from meltygui.core.cache.tile_cache import clear_glows
|
|
1014
|
+
from meltygui.core.layout.column_core import ColumnLayout
|
|
1015
|
+
|
|
1016
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1017
|
+
folder_icon = f""
|
|
1018
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1019
|
+
computer_icon = f""
|
|
1020
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1021
|
+
left_pad = 8.0
|
|
1022
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1023
|
+
glyph_width = 20.0
|
|
1024
|
+
text_rgba = (0.85, 0.88, 0.92, 1.0)
|
|
1025
|
+
text_col = pack_color(*text_rgba)
|
|
1026
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1027
|
+
hover_boost = 0.06
|
|
1028
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1029
|
+
hover_alpha = 0.05
|
|
1030
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1031
|
+
text_mix = 0.5
|
|
1032
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1033
|
+
icon_mix = 0.9
|
|
1034
|
+
hover_wash = pack_color(1.0, 1.0, 1.0, hover_alpha)
|
|
1035
|
+
|
|
1036
|
+
px = Melty.px
|
|
1037
|
+
clear_glows(draw_state) # The current shortcut's retained shadow (see the listing)
|
|
1038
|
+
directory = Path(input_value if input_value else Path.home()).expanduser()
|
|
1039
|
+
draw_list = imgui.get_window_draw_list()
|
|
1040
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
1041
|
+
hover_ok = draw_state._bounding_hovered
|
|
1042
|
+
click = ((left_mouse_clicked.x, left_mouse_clicked.y)
|
|
1043
|
+
if (left_mouse_clicked and hasattr(left_mouse_clicked, "x")) else None)
|
|
1044
|
+
chip = px(chip_size) if show_tint_chips else 0.0
|
|
1045
|
+
text_x = px(left_pad) + (chip + px(6) if show_tint_chips else 0.0)
|
|
1046
|
+
meta = file_metadata
|
|
1047
|
+
row_bg = row_tint_bg()
|
|
1048
|
+
# The listing writes what the right-click landed on here each run; the
|
|
1049
|
+
# menu item callables (built here, run by the wrapper's items menu with no
|
|
1050
|
+
# label) read it when picked.
|
|
1051
|
+
menu_target = {"path": str(directory)}
|
|
1052
|
+
menu_items = None
|
|
1053
|
+
if context_menu:
|
|
1054
|
+
menu_items = {label: (lambda _action=action: _action(menu_target["path"]))
|
|
1055
|
+
for label, action in context_menu.items()}
|
|
1056
|
+
|
|
1057
|
+
# The band: from the flow cursor to the bottom of the view.
|
|
1058
|
+
body_top = imgui.get_cursor_screen_pos()[1]
|
|
1059
|
+
body_bottom = draw_state.abs_top + (draw_state.height or px(600))
|
|
1060
|
+
body_height = max(px(120), body_bottom - body_top)
|
|
1061
|
+
columns = ColumnLayout(draw_state, 2, column_edges=column_edges,
|
|
1062
|
+
column_widths=[shortcuts_width, None], column_mins=[110, 200],
|
|
1063
|
+
padding=px(column_gap), padding_y=0, border_color=None)
|
|
1064
|
+
|
|
1065
|
+
result = (False, input_value)
|
|
1066
|
+
picked = None
|
|
1067
|
+
with columns.cell(0, height=body_height) as width:
|
|
1068
|
+
changed_s, picked_s = draw_shortcuts(
|
|
1069
|
+
str(directory), name="shortcuts", file_metadata=file_metadata, width=width, height=body_height,
|
|
1070
|
+
row_height=shortcut_row_height, show_tint_chips=show_tint_chips,
|
|
1071
|
+
chip_size=chip_size, default_tint=default_tint, disable_scroll=True,
|
|
1072
|
+
left_mouse_clicked=left_mouse_clicked)
|
|
1073
|
+
picked = Path(picked_s) if changed_s else None
|
|
1074
|
+
with columns.cell(1, height=body_height) as width:
|
|
1075
|
+
if layout_out is not None:
|
|
1076
|
+
layout_out["listing_left"] = imgui.get_cursor_screen_pos()[0]
|
|
1077
|
+
changed, value = draw_file_listing(str(directory), name="listing", file_metadata=file_metadata, width=width,
|
|
1078
|
+
height=body_height, disable_scroll=False,
|
|
1079
|
+
context_menu=menu_items, menu_target=menu_target,
|
|
1080
|
+
drag_rows=drag_rows, folder_bg_boost=folder_bg_boost,
|
|
1081
|
+
folder_bg_rounding=folder_bg_rounding,
|
|
1082
|
+
show_tint_chips=show_tint_chips, chip_size=chip_size,
|
|
1083
|
+
default_tint=default_tint, type_to_search=type_to_search,
|
|
1084
|
+
show_crumbs=show_crumbs, show_hidden=show_hidden)
|
|
1085
|
+
if changed:
|
|
1086
|
+
result = (True, value)
|
|
1087
|
+
columns.finish()
|
|
1088
|
+
|
|
1089
|
+
if result[0]:
|
|
1090
|
+
return result
|
|
1091
|
+
if picked is not None:
|
|
1092
|
+
request_render()
|
|
1093
|
+
return True, str(picked)
|
|
1094
|
+
if ctrl_up_key_pressed and directory.parent != directory:
|
|
1095
|
+
request_render()
|
|
1096
|
+
return True, str(directory.parent)
|
|
1097
|
+
return False, input_value
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
@render_func(tint=(0.32, 0.42, 0.54), selectable=False, disable_scroll=True,
|
|
1101
|
+
show_bg=False, shadow=False, determines_height=False)
|
|
1102
|
+
def draw_file_selector(input_value: str | None = None, draw_state=None,
|
|
1103
|
+
selector_state: FileSelectorState = None,
|
|
1104
|
+
escape_key_pressed=False, choose_folder=False,
|
|
1105
|
+
context_menu=None, browse=None, show_hidden=None, file_metadata=None):
|
|
1106
|
+
"""Return (True, absolute_path) once when a file is activated.
|
|
1107
|
+
|
|
1108
|
+
Navigation stays here. input_value seeds the initial directory (home
|
|
1109
|
+
by default); later opens remember the last directory. Double-click or
|
|
1110
|
+
Enter selects a file. Cancel / the window close button returns no change.
|
|
1111
|
+
In an OS child, selection closes the window. Pass open_requested=True
|
|
1112
|
+
for one frame to open/reopen it; False leaves its current state alone.
|
|
1113
|
+
Omit open_requested to open immediately on the first call.
|
|
1114
|
+
|
|
1115
|
+
``choose_folder=True`` makes it a FOLDER picker: files are inert, a
|
|
1116
|
+
"Choose Folder" button beside Cancel returns the directory being
|
|
1117
|
+
shown (double-click still descends). ``context_menu`` = {label:
|
|
1118
|
+
callable(path)} is the listing's right-click menu (the explorer's).
|
|
1119
|
+
``browse`` = a directory to navigate to — a path or ``(path, token)``
|
|
1120
|
+
— applied once per distinct value; keep passing it.
|
|
1121
|
+
"""
|
|
1122
|
+
from meltygui.core.runtime.app import pressed
|
|
1123
|
+
from meltygui.view.control_view import draw_button
|
|
1124
|
+
from meltygui.view.file_view import draw_fast_file_explorer
|
|
1125
|
+
|
|
1126
|
+
if selector_state.directory is None:
|
|
1127
|
+
directory = Path(input_value or Path.home()).expanduser().resolve()
|
|
1128
|
+
selector_state.directory = str(directory if directory.is_dir() else directory.parent)
|
|
1129
|
+
# ``browse``: a directory to show - a path, or ``(path, token)`` where
|
|
1130
|
+
# a new token re-applies the same path - applied once per distinct
|
|
1131
|
+
# value (the settings editor's shortcuts column pick). An OS-child body
|
|
1132
|
+
# runs on its own thread, so the host keeps passing the token rather
|
|
1133
|
+
# than passing it for one frame.
|
|
1134
|
+
if browse != getattr(selector_state, "_browse_applied", None):
|
|
1135
|
+
selector_state._browse_applied = browse
|
|
1136
|
+
target = browse[0] if isinstance(browse, tuple) else browse
|
|
1137
|
+
if target:
|
|
1138
|
+
target = Path(target).expanduser()
|
|
1139
|
+
if target.is_dir():
|
|
1140
|
+
selector_state.directory = str(target)
|
|
1141
|
+
left, top, right, bottom = draw_state.get_content_rect()
|
|
1142
|
+
_trace_browser_size("file-selector-size", draw_state,
|
|
1143
|
+
content=(left, top, right, bottom),
|
|
1144
|
+
explorer=(right - left, max(120, bottom - top - 35)))
|
|
1145
|
+
changed, picked = draw_fast_file_explorer(
|
|
1146
|
+
selector_state.directory, name='files', width=right - left,
|
|
1147
|
+
file_metadata=file_metadata,
|
|
1148
|
+
height=max(120, bottom - top - 35),
|
|
1149
|
+
folder_bg_boost=-0.23, folder_bg_rounding=10.0, context_menu=context_menu,
|
|
1150
|
+
show_hidden=show_hidden)
|
|
1151
|
+
# The explorer's flow cursor includes the full listing height, even
|
|
1152
|
+
# when that listing is clipped/scrolling. Anchor the reserved footer
|
|
1153
|
+
# to the selector's viewport so resizing cannot push it off-window.
|
|
1154
|
+
imgui.set_cursor_screen_pos((left, bottom - 30))
|
|
1155
|
+
if choose_folder:
|
|
1156
|
+
chosen, _ = draw_button(label='Choose Folder', name='choose', show_header=False,
|
|
1157
|
+
width=130, height=25)
|
|
1158
|
+
imgui.same_line()
|
|
1159
|
+
if chosen:
|
|
1160
|
+
draw_state.closed = True
|
|
1161
|
+
return True, selector_state.directory
|
|
1162
|
+
cancelled, _ = draw_button(label='Cancel', name='cancel', show_header=False,
|
|
1163
|
+
width=90, height=25)
|
|
1164
|
+
if cancelled or escape_key_pressed or pressed('escape'):
|
|
1165
|
+
draw_state.closed = True
|
|
1166
|
+
return False, None
|
|
1167
|
+
if changed:
|
|
1168
|
+
path = Path(picked).expanduser().resolve()
|
|
1169
|
+
if path.is_dir():
|
|
1170
|
+
selector_state.directory = str(path)
|
|
1171
|
+
elif path.is_file() and not choose_folder:
|
|
1172
|
+
draw_state.closed = True
|
|
1173
|
+
return True, str(path)
|
|
1174
|
+
return False, None
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
@render_func()
|
|
1178
|
+
def file_watch_debug(draw_state=None):
|
|
1179
|
+
from meltygui.core.files.file_watch_core import _symbol_index_view
|
|
1180
|
+
|
|
1181
|
+
from meltygui.core.melty import Melty
|
|
1182
|
+
from meltygui.core.melty import FileWatch
|
|
1183
|
+
from meltygui.editor.external_changes import ExternalChanges
|
|
1184
|
+
|
|
1185
|
+
RenderFuncs.draw_function(FileWatch.watch_project_files, icon="",
|
|
1186
|
+
tint=(0, 0, 0, 1), show_bg=False, run_in_thread=True,
|
|
1187
|
+
result_fade_frames=30)
|
|
1188
|
+
|
|
1189
|
+
from meltygui.editor.pending_save import PendingSave
|
|
1190
|
+
|
|
1191
|
+
view_paths = {p: len(dss) for p, dss in FileWatch.path_to_draw_states.items()}
|
|
1192
|
+
tracked = FileWatch.project_tracked
|
|
1193
|
+
all_paths = sorted(set(view_paths) | set(tracked))
|
|
1194
|
+
|
|
1195
|
+
refs_cache, span_cache, snap, index_gen = _symbol_index_view()
|
|
1196
|
+
spans_by_file = defaultdict(list)
|
|
1197
|
+
for (p, start, end), (sig, _usages) in list(span_cache.items()):
|
|
1198
|
+
spans_by_file[p].append(sig)
|
|
1199
|
+
|
|
1200
|
+
_mtimes = {}
|
|
1201
|
+
|
|
1202
|
+
def _mtime(p):
|
|
1203
|
+
if p not in _mtimes:
|
|
1204
|
+
try:
|
|
1205
|
+
_mtimes[p] = os.stat(p).st_mtime
|
|
1206
|
+
except OSError:
|
|
1207
|
+
_mtimes[p] = None
|
|
1208
|
+
return _mtimes[p]
|
|
1209
|
+
|
|
1210
|
+
def _sym_marks(p):
|
|
1211
|
+
"""Usage-symbol status for one file: warmer refs cache + span usages."""
|
|
1212
|
+
out = []
|
|
1213
|
+
entry = refs_cache.get(p)
|
|
1214
|
+
if entry is not None:
|
|
1215
|
+
# entry[0] is (mtime, pending_gen); disk-freshness is the mtime
|
|
1216
|
+
# half. A pre-hotswap twin module can still hold old-format bare
|
|
1217
|
+
# float entries - treat those as the mtime too.
|
|
1218
|
+
_sig = entry[0]
|
|
1219
|
+
fresh = (_sig[0] if isinstance(_sig, tuple) else _sig) == _mtime(p)
|
|
1220
|
+
out.append(f"refs {len(entry[1])}" if fresh else "refs STALE")
|
|
1221
|
+
elif p in snap:
|
|
1222
|
+
out.append("refs evicted") # counted by the warmer, but cold
|
|
1223
|
+
else:
|
|
1224
|
+
out.append("no idx") # never indexed (no live module)
|
|
1225
|
+
sigs = spans_by_file.get(p)
|
|
1226
|
+
if sigs:
|
|
1227
|
+
try:
|
|
1228
|
+
pg = PendingSave.pending_gen_for(Path(p))
|
|
1229
|
+
ok = sum(1 for s in sigs
|
|
1230
|
+
if s and s[0] == _mtime(p) and s[1] == pg
|
|
1231
|
+
and (s[2] or s[3] == index_gen))
|
|
1232
|
+
except Exception:
|
|
1233
|
+
ok = "?"
|
|
1234
|
+
out.append(f"usages {ok}/{len(sigs)} fresh")
|
|
1235
|
+
return out
|
|
1236
|
+
|
|
1237
|
+
refs_fresh = sum(1 for p, entry in refs_cache.items()
|
|
1238
|
+
if entry is not None and entry[0] == _mtime(p))
|
|
1239
|
+
cached = sum(1 for p in all_paths if p in Melty.code_cache)
|
|
1240
|
+
lines = [
|
|
1241
|
+
f"watched dirs: {len(FileWatch._watched_dirs)} "
|
|
1242
|
+
f"files known: {len(all_paths)} (project-tracked {len(tracked)}, "
|
|
1243
|
+
f"view-watched {len(view_paths)})",
|
|
1244
|
+
f"baselined in code_cache: {cached}/{len(all_paths)} "
|
|
1245
|
+
f"external drift tracked: {len(ExternalChanges.originals)}",
|
|
1246
|
+
f"symbol index: gen {index_gen} refs cached {len(refs_cache)} file(s) "
|
|
1247
|
+
f"({refs_fresh} fresh) span usages {len(span_cache)} "
|
|
1248
|
+
f"across {len(spans_by_file)} file(s)",
|
|
1249
|
+
"",
|
|
1250
|
+
]
|
|
1251
|
+
|
|
1252
|
+
by_dir = defaultdict(list)
|
|
1253
|
+
for p in all_paths:
|
|
1254
|
+
by_dir[str(Path(p).parent)].append(p)
|
|
1255
|
+
|
|
1256
|
+
# Dirs watched but currently holding no tracked file still matter (events
|
|
1257
|
+
# within them can auto-track new .py files) - show them at the end.
|
|
1258
|
+
empty_dirs = sorted(d for d in FileWatch._watched_dirs if d not in by_dir)
|
|
1259
|
+
|
|
1260
|
+
for d in sorted(by_dir):
|
|
1261
|
+
watched = "" if d in FileWatch._watched_dirs else " [dir NOT scheduled!]"
|
|
1262
|
+
lines.append(f"{d}{watched}")
|
|
1263
|
+
for p in by_dir[d]:
|
|
1264
|
+
marks = []
|
|
1265
|
+
if p in Melty.code_cache:
|
|
1266
|
+
marks.append("cached")
|
|
1267
|
+
else:
|
|
1268
|
+
marks.append("UNCACHED — edits invisible")
|
|
1269
|
+
n = view_paths.get(p)
|
|
1270
|
+
if n:
|
|
1271
|
+
marks.append(f"{n} view{'s' if n > 1 else ''}")
|
|
1272
|
+
if p in tracked:
|
|
1273
|
+
marks.append("tracked")
|
|
1274
|
+
if p in ExternalChanges.originals:
|
|
1275
|
+
marks.append("DRIFT")
|
|
1276
|
+
marks.extend(_sym_marks(p))
|
|
1277
|
+
lines.append(f" {Path(p).name} [{', '.join(marks)}]")
|
|
1278
|
+
if empty_dirs:
|
|
1279
|
+
lines.append("")
|
|
1280
|
+
lines.append(f"watched dirs with no known files ({len(empty_dirs)}):")
|
|
1281
|
+
lines.extend(f" {d}" for d in empty_dirs)
|
|
1282
|
+
|
|
1283
|
+
RenderFuncs.draw_text("\n".join(lines), show_name=True, name="watch status")
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
@render_func(tint=(0.32, 0.42, 0.54), auto_resize=False, selectable=False)
|
|
1287
|
+
def render_file_tree(input_value=None, draw_state=None,
|
|
1288
|
+
file_tree_state: FileTreeState = None, root=ROOT, file_metadata=None,
|
|
1289
|
+
left_mouse_down=False, left_mouse_double_clicked=False,
|
|
1290
|
+
escape_key_pressed=False, **kwargs):
|
|
1291
|
+
# Geometry authored at ui_scale 1.0 — scaled through Melty.px per frame.
|
|
1292
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1293
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1294
|
+
from meltygui.view.header_view import flat_button
|
|
1295
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
1296
|
+
from meltygui.core.input.drag_drop_core import DragDrop
|
|
1297
|
+
from meltygui.core.layout.header_runtime import _brightness_clamp_fn
|
|
1298
|
+
from meltygui.model.import_graph_model import start_build
|
|
1299
|
+
from meltygui.core.files.file_tree_core import _apply_row_drop
|
|
1300
|
+
from meltygui.core.files.file_tree_core import _flatten_ordered
|
|
1301
|
+
from meltygui.core.files.file_tree_core import _tint_of
|
|
1302
|
+
import meltygui.model.import_graph_model as file_graph
|
|
1303
|
+
|
|
1304
|
+
row_height = 20.0
|
|
1305
|
+
# Per nesting level, in px at ui_scale 1.0 — kept tiny so a deep tree
|
|
1306
|
+
# doesn't march off to the right (Lukas 09-04: 2 px, no stair).
|
|
1307
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1308
|
+
indent_per_level = 2.0
|
|
1309
|
+
left_pad = 6.0
|
|
1310
|
+
glyph_width = 12.0
|
|
1311
|
+
# Row backgrounds are the files' own tints (FileMeta) run through the
|
|
1312
|
+
# SAME colour pipeline as the editor's active tabs (flat_button: the
|
|
1313
|
+
# style manager's mix under Toggles.CodeEditor.tab_active_bg_* + the
|
|
1314
|
+
# brightness clamp), so every row bg stays dark enough for light text.
|
|
1315
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1316
|
+
bg_theme_factor = 0.1
|
|
1317
|
+
# With an import graph built every file rises with its USAGE — importer
|
|
1318
|
+
# count on the graph's log scale (ImportGraph.usage, 0..1) — through a
|
|
1319
|
+
# drop shadow (add_shadow depth lift, the editor's active-tab mechanism)
|
|
1320
|
+
# AND its name's font size, so the files everything depends on pop out
|
|
1321
|
+
# of the tree. While a file is SELECTED only it and its highlighted files
|
|
1322
|
+
# cast: the selection and its IMPORTERS are RAISED by highlight_lift, its
|
|
1323
|
+
# IMPORTS are RECESSED by the same amount (a negative add_shadow offset
|
|
1324
|
+
# carves the row in, the surroundings cast into it), every other file
|
|
1325
|
+
# drops flat; the font keeps following usage throughout. A file that is
|
|
1326
|
+
# both reads as an importer.
|
|
1327
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1328
|
+
usage_lift = 6.0
|
|
1329
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1330
|
+
highlight_lift = 3.0
|
|
1331
|
+
# Font scale range over usage (clamped so the biggest still fits the
|
|
1332
|
+
# row); a file NOBODY imports is greyed out.
|
|
1333
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1334
|
+
usage_font_scale_min = 0.9
|
|
1335
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1336
|
+
usage_font_scale_max = 1.3
|
|
1337
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1338
|
+
unused_text = (0.55, 0.57, 0.6, 0.6)
|
|
1339
|
+
# Import-graph highlights wear the SELECTED file's own tint: what it
|
|
1340
|
+
# imports in the tint itself, what imports it in a lighter, washed-out
|
|
1341
|
+
# variant (importer_value_boost / importer_saturation), both blended
|
|
1342
|
+
# for a file in both directions. A folder holding hidden hits gets the
|
|
1343
|
+
# same mark at reduced alpha. An unpainted selection uses the fallback.
|
|
1344
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1345
|
+
highlight_fallback_tint = (0.55, 0.72, 0.95)
|
|
1346
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1347
|
+
importer_value_boost = 0.35
|
|
1348
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1349
|
+
importer_saturation = 0.45
|
|
1350
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1351
|
+
highlight_bar_width = 3.0
|
|
1352
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
1353
|
+
toolbar_height = 26.0
|
|
1354
|
+
|
|
1355
|
+
state = file_tree_state
|
|
1356
|
+
px = Melty.px
|
|
1357
|
+
row_h, indent, pad, glyph_w = px(row_height), px(indent_per_level), px(left_pad), px(glyph_width)
|
|
1358
|
+
|
|
1359
|
+
dl = imgui.get_window_draw_list()
|
|
1360
|
+
cw = draw_state.content_width or (draw_state.width or 240)
|
|
1361
|
+
|
|
1362
|
+
# ── toolbar: the import-graph build button + status ───────────────────
|
|
1363
|
+
build = state._build
|
|
1364
|
+
if build is not None and not build.running:
|
|
1365
|
+
if build.result is not None:
|
|
1366
|
+
state._graph = build.result
|
|
1367
|
+
state._build = None
|
|
1368
|
+
draw_state.invalidate()
|
|
1369
|
+
if state._graph is None and file_graph.current() is not None:
|
|
1370
|
+
state._graph = file_graph.current() # built by the graph graph
|
|
1371
|
+
draw_state.invalidate()
|
|
1372
|
+
if flat_button("Build import graph##file_tree", draw_state,
|
|
1373
|
+
view_id="file_tree_build_graph", height=px(toolbar_height) - px(4),
|
|
1374
|
+
event="left_mouse_down"):
|
|
1375
|
+
if state._build is None:
|
|
1376
|
+
state._build = start_build(Path(root))
|
|
1377
|
+
request_render()
|
|
1378
|
+
status_x, status_y = imgui.get_item_rect_max().x + px(8), imgui.get_item_rect_min().y + px(4)
|
|
1379
|
+
graph = state._graph
|
|
1380
|
+
if build is not None and build.running:
|
|
1381
|
+
status = "building…"
|
|
1382
|
+
elif build is not None and build.error:
|
|
1383
|
+
status = build.error
|
|
1384
|
+
elif graph is not None:
|
|
1385
|
+
status = f"{graph.file_count} files, {graph.edge_count} imports, {graph.seconds:.1f}s"
|
|
1386
|
+
if graph.errors:
|
|
1387
|
+
status += f", {len(graph.errors)} unparsed"
|
|
1388
|
+
else:
|
|
1389
|
+
status = "no graph yet"
|
|
1390
|
+
dl.add_text(status_x, status_y, pack_color(0.75, 0.78, 0.82, 1.0), status)
|
|
1391
|
+
imgui.set_cursor_pos_y(imgui.get_cursor_pos_y() + px(4))
|
|
1392
|
+
x0, y0 = imgui.get_cursor_screen_pos()
|
|
1393
|
+
|
|
1394
|
+
# Esc (while the tree is open) clears the selection - and with it the
|
|
1395
|
+
# highlights and the fixed-lift shadows.
|
|
1396
|
+
if escape_key_pressed and state.selected is not None:
|
|
1397
|
+
state.select(None)
|
|
1398
|
+
request_render()
|
|
1399
|
+
|
|
1400
|
+
# Highlight sets for the selected file (files only - folders are marked
|
|
1401
|
+
# when they hold a hit that isn't visible).
|
|
1402
|
+
imports, importers = set(), set()
|
|
1403
|
+
selected_path = state.selected_path
|
|
1404
|
+
if graph is not None and selected_path is not None:
|
|
1405
|
+
imports = graph.imports_of(selected_path)
|
|
1406
|
+
importers = graph.importers_of(selected_path)
|
|
1407
|
+
hits = imports | importers
|
|
1408
|
+
|
|
1409
|
+
# Row order comes from the file_meta dict: a path's key position is its
|
|
1410
|
+
# rank among its siblings (drag-reorder rewrites those positions).
|
|
1411
|
+
meta = file_metadata
|
|
1412
|
+
position = ({k: i for i, k in enumerate(meta)} if meta is not None else {})
|
|
1413
|
+
rows = _flatten_ordered(Path(root), state.expanded, meta, position)
|
|
1414
|
+
# One dummy reports the total height so the container scrolls normally.
|
|
1415
|
+
# The scroll clamp is content_height - clipped_height, but clipped_height
|
|
1416
|
+
# spans the WHOLE window (header included) while the rows start below the
|
|
1417
|
+
# header + toolbar - pad the reported height by that top inset (the
|
|
1418
|
+
# fast_dock rule) or the last row can never scroll fully into view.
|
|
1419
|
+
top_inset = (y0 + draw_state.scroll_offset[1]) - draw_state.abs_top
|
|
1420
|
+
imgui.dummy(cw, max(1.0, len(rows) * row_h + max(0.0, top_inset)))
|
|
1421
|
+
selected_tint = _tint_of(meta, selected_path) if selected_path is not None else None
|
|
1422
|
+
imports_tint = selected_tint or highlight_fallback_tint
|
|
1423
|
+
hue, saturation, value = colorsys.rgb_to_hsv(*imports_tint)
|
|
1424
|
+
importers_tint = colorsys.hsv_to_rgb(hue, saturation * importer_saturation,
|
|
1425
|
+
min(1.0, value + importer_value_boost))
|
|
1426
|
+
|
|
1427
|
+
mx, my = imgui.get_mouse_pos()
|
|
1428
|
+
hover_ok = draw_state._bounding_hovered
|
|
1429
|
+
click = ((left_mouse_down.x, left_mouse_down.y)
|
|
1430
|
+
if (left_mouse_down and hasattr(left_mouse_down, "x")) else None)
|
|
1431
|
+
dclick = ((left_mouse_double_clicked.x, left_mouse_double_clicked.y)
|
|
1432
|
+
if (left_mouse_double_clicked and hasattr(left_mouse_double_clicked, "x")) else None)
|
|
1433
|
+
clip = getattr(draw_state, "abs_clip_rect", None)
|
|
1434
|
+
|
|
1435
|
+
text_col = pack_color(0.92, 0.92, 0.92, 1.0)
|
|
1436
|
+
unused_col = pack_color(*unused_text)
|
|
1437
|
+
font_size = imgui.get_font_size()
|
|
1438
|
+
# The scaled name must still fit the row.
|
|
1439
|
+
font_scale_max = min(usage_font_scale_max, row_h / max(font_size, 1.0))
|
|
1440
|
+
style_manager = Melty.style_manager
|
|
1441
|
+
brightness_clamp = _brightness_clamp_fn()
|
|
1442
|
+
bg_memo = {}
|
|
1443
|
+
|
|
1444
|
+
def row_bg(tint):
|
|
1445
|
+
"""The tab bar's bg colour for `tint` (memoized per body run)."""
|
|
1446
|
+
bg = bg_memo.get(tint)
|
|
1447
|
+
if bg is None:
|
|
1448
|
+
mixed = style_manager.make_color_rgb(
|
|
1449
|
+
tint[0], tint[1], tint[2],
|
|
1450
|
+
value=Toggles.CodeEditor.tab_active_bg_brightness,
|
|
1451
|
+
factor=bg_theme_factor,
|
|
1452
|
+
saturation_scale=Toggles.CodeEditor.tab_active_bg_saturation,
|
|
1453
|
+
alpha=1.0)
|
|
1454
|
+
mixed = brightness_clamp(mixed[0], mixed[1], mixed[2], 0.0,
|
|
1455
|
+
Toggles.CodeEditor.tab_active_bg_max_brightness)
|
|
1456
|
+
bg = bg_memo[tint] = pack_color(mixed[0], mixed[1], mixed[2], 1.0)
|
|
1457
|
+
return bg
|
|
1458
|
+
hover_col = pack_color(1.0, 1.0, 1.0, 0.08)
|
|
1459
|
+
select_col = pack_color(0.4, 0.6, 0.9, 0.35)
|
|
1460
|
+
|
|
1461
|
+
imports_col = pack_color(*imports_tint, 0.9)
|
|
1462
|
+
importers_col = pack_color(*importers_tint, 0.9)
|
|
1463
|
+
both_col = pack_color(*[(a + b) / 2 for a, b in zip(imports_tint, importers_tint)], 0.9)
|
|
1464
|
+
imports_wash = pack_color(*imports_tint, 0.14)
|
|
1465
|
+
importers_wash = pack_color(*importers_tint, 0.14)
|
|
1466
|
+
folder_alpha = 0.45
|
|
1467
|
+
|
|
1468
|
+
def highlight_of(p):
|
|
1469
|
+
"""(bar colour, wash colour, alpha scale) for a row, or None."""
|
|
1470
|
+
if not hits:
|
|
1471
|
+
return None
|
|
1472
|
+
if p.is_dir():
|
|
1473
|
+
inside = [h for h in hits if p in h.parents]
|
|
1474
|
+
if not inside:
|
|
1475
|
+
return None
|
|
1476
|
+
in_imports = any(h in imports for h in inside)
|
|
1477
|
+
in_importers = any(h in importers for h in inside)
|
|
1478
|
+
scale = folder_alpha
|
|
1479
|
+
else:
|
|
1480
|
+
in_imports, in_importers = p in imports, p in importers
|
|
1481
|
+
if not (in_imports or in_importers):
|
|
1482
|
+
return None
|
|
1483
|
+
scale = 1.0
|
|
1484
|
+
if in_imports and in_importers:
|
|
1485
|
+
return both_col, imports_wash, scale
|
|
1486
|
+
if in_imports:
|
|
1487
|
+
return imports_col, imports_wash, scale
|
|
1488
|
+
return importers_col, importers_wash, scale
|
|
1489
|
+
|
|
1490
|
+
def paint_row(draw_list, rx, ry, p, depth, tint, hovered, selected, shadow=True):
|
|
1491
|
+
"""One row's visuals at (rx, ry): tint bg, hover/select wash, folder
|
|
1492
|
+
chevron, name. Shared by the inline rows and the drag ghost (which
|
|
1493
|
+
rides the overlay list outside the shadow marks' clip: shadow=False)."""
|
|
1494
|
+
# The bg is indented with the text: it starts at the row's chevron
|
|
1495
|
+
# column and runs to the right edge.
|
|
1496
|
+
x = rx + pad + depth * indent
|
|
1497
|
+
usage = None # None if no graph / a folder
|
|
1498
|
+
if graph is not None and not p.is_dir():
|
|
1499
|
+
usage = graph.usage(p)
|
|
1500
|
+
if shadow and usage is not None:
|
|
1501
|
+
if selected_path is not None:
|
|
1502
|
+
if p == selected_path or p in importers:
|
|
1503
|
+
lift = highlight_lift
|
|
1504
|
+
elif p in imports:
|
|
1505
|
+
lift = -highlight_lift
|
|
1506
|
+
else:
|
|
1507
|
+
lift = 0.0
|
|
1508
|
+
else:
|
|
1509
|
+
lift = usage_lift * usage
|
|
1510
|
+
if lift != 0.0:
|
|
1511
|
+
add_shadow((x, ry, rx + cw - x, row_h), offset=lift, corner_radius=0.0)
|
|
1512
|
+
if tint is not None:
|
|
1513
|
+
draw_list.add_rect_filled(x, ry, rx + cw, ry + row_h, row_bg(tint))
|
|
1514
|
+
if selected:
|
|
1515
|
+
draw_list.add_rect_filled(x, ry, rx + cw, ry + row_h, select_col)
|
|
1516
|
+
if hovered:
|
|
1517
|
+
draw_list.add_rect_filled(x, ry, rx + cw, ry + row_h, hover_col)
|
|
1518
|
+
mark = highlight_of(p) if shadow else None
|
|
1519
|
+
if mark is not None:
|
|
1520
|
+
bar_col, wash_col, scale = mark
|
|
1521
|
+
if scale < 1.0:
|
|
1522
|
+
bar_col = with_alpha(bar_col, 0.9 * scale)
|
|
1523
|
+
wash_col = with_alpha(wash_col, 0.14 * scale)
|
|
1524
|
+
draw_list.add_rect_filled(x, ry, rx + cw, ry + row_h, wash_col)
|
|
1525
|
+
draw_list.add_rect_filled(x, ry, x + px(highlight_bar_width), ry + row_h, bar_col)
|
|
1526
|
+
if p.is_dir():
|
|
1527
|
+
cx, cy = x + px(4.0), ry + row_h / 2
|
|
1528
|
+
radius = px(4.0)
|
|
1529
|
+
tri = ([(cx - radius + 1, cy - radius), (cx + radius - 1, cy), (cx - radius + 1, cy + radius)]
|
|
1530
|
+
if not state.is_expanded(p)
|
|
1531
|
+
else [(cx - radius, cy - radius + 1), (cx + radius, cy - radius + 1), (cx, cy + radius - 1)])
|
|
1532
|
+
draw_list.add_triangle_filled(*tri[0], *tri[1], *tri[2], text_col)
|
|
1533
|
+
name_col = unused_col if usage == 0.0 else text_col
|
|
1534
|
+
if usage is not None:
|
|
1535
|
+
# set_window_font_scale retargets the draw list's font size at
|
|
1536
|
+
# once (imgui's AddText uses g.Fonts), so the name scales
|
|
1537
|
+
# without a second font face; reset right after.
|
|
1538
|
+
scale = usage_font_scale_min + (font_scale_max - usage_font_scale_min) * usage
|
|
1539
|
+
imgui.set_window_font_scale(scale)
|
|
1540
|
+
draw_list.add_text(x + glyph_w, ry + (row_h - font_size * scale) * 0.5, name_col, p.name)
|
|
1541
|
+
imgui.set_window_font_scale(1.0)
|
|
1542
|
+
else:
|
|
1543
|
+
draw_list.add_text(x + glyph_w, ry + px(2.0), name_col, p.name)
|
|
1544
|
+
|
|
1545
|
+
# on_drag call order == index into `visible` (DropEvent indices count
|
|
1546
|
+
# on_drag calls, so only rows that were used as handles are indexed).
|
|
1547
|
+
visible = []
|
|
1548
|
+
for i, (p, depth) in enumerate(rows):
|
|
1549
|
+
ry0 = y0 + i * row_h
|
|
1550
|
+
ry1 = ry0 + row_h
|
|
1551
|
+
|
|
1552
|
+
def in_row(pt):
|
|
1553
|
+
return pt is not None and x0 <= pt[0] <= x0 + cw and ry0 <= pt[1] <= ry1
|
|
1554
|
+
|
|
1555
|
+
if p.is_dir():
|
|
1556
|
+
if in_row(click):
|
|
1557
|
+
state.toggle(p)
|
|
1558
|
+
request_render()
|
|
1559
|
+
else:
|
|
1560
|
+
if in_row(click):
|
|
1561
|
+
state.select(p)
|
|
1562
|
+
request_render()
|
|
1563
|
+
if in_row(dclick):
|
|
1564
|
+
state.open_file(p)
|
|
1565
|
+
|
|
1566
|
+
if clip is not None and (ry1 < clip[1] or ry0 > clip[3]):
|
|
1567
|
+
continue
|
|
1568
|
+
|
|
1569
|
+
visible.append((p, depth))
|
|
1570
|
+
tint = _tint_of(meta, p)
|
|
1571
|
+
# Immediate-mode DragDrop (the editor tab bar's model): the row rect
|
|
1572
|
+
# is its own drag handle. While THIS row is the active drag - on_drag
|
|
1573
|
+
# has parked the cursor at the drop position on the overlay list -
|
|
1574
|
+
# paint the same row there and leave the inline slot empty (the
|
|
1575
|
+
# home/cancel target; DragDrop draws the feedback lines).
|
|
1576
|
+
drag = DragDrop.on_drag((x0, ry0, x0 + cw, ry1), key=str(p),
|
|
1577
|
+
draw_state=draw_state)
|
|
1578
|
+
if drag:
|
|
1579
|
+
paint_row(drag.draw_list, drag.x, drag.y, p, depth, tint,
|
|
1580
|
+
hovered=False, selected=False, shadow=False)
|
|
1581
|
+
DragDrop.end_drag()
|
|
1582
|
+
continue
|
|
1583
|
+
|
|
1584
|
+
hovered = hover_ok and x0 <= mx <= x0 + cw and ry0 <= my <= ry1
|
|
1585
|
+
paint_row(dl, x0, ry0, p, depth, tint, hovered, p == selected_path)
|
|
1586
|
+
|
|
1587
|
+
# A landed drop reorders within ONE folder: the slot must be beside a
|
|
1588
|
+
# sibling of the dragged row (before the row at insert_index, or after
|
|
1589
|
+
# the row above it); anything else - another folder, a cross-collection
|
|
1590
|
+
# kind - is ignored.
|
|
1591
|
+
drop = DragDrop.on_drop(horizontal=False, draw_state=draw_state)
|
|
1592
|
+
if drop is not None and drop.kind == "reorder" and drop.index is not None:
|
|
1593
|
+
dragged = visible[drop.index][0] if 0 <= drop.index < len(visible) else None
|
|
1594
|
+
if dragged is not None and _apply_row_drop(meta, dragged, visible,
|
|
1595
|
+
drop.insert_index, position):
|
|
1596
|
+
draw_state.invalidate()
|
|
1597
|
+
request_render()
|
|
1598
|
+
|
|
1599
|
+
return False, None
|