diff --git a/addons/asset_drawer/LICENSE b/addons/asset_drawer/LICENSE deleted file mode 100644 index 1343f65..0000000 --- a/addons/asset_drawer/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Zalyto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/addons/asset_drawer/README.md b/addons/asset_drawer/README.md deleted file mode 100644 index 121cde1..0000000 --- a/addons/asset_drawer/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# Asset Drawer - - - -> An asset drawer inspired by **Unreal Engine's Content Drawer**, for Godot 4. -> Press **Ctrl+Space** and the drawer slides up from the bottom of the editor. Browse, search, drag and drop, then close it. - -![Godot 4.2+](https://img.shields.io/badge/Godot-4.2%2B-478cbf?logo=godotengine&logoColor=white) -![Version](https://img.shields.io/badge/version-3.6-blue) -![Type](https://img.shields.io/badge/type-editor%20plugin-lightgrey) - - - ---- - -## Table of contents - -- [Why](#why) -- [Installation](#installation) -- [Quick start](#quick-start) -- [Shortcuts](#shortcuts) -- [Features](#features) -- [Right-click menu](#right-click-menu) -- [Copy / paste a resource](#copy--paste-a-resource) -- [Export an image of the drawer](#export-an-image-of-the-drawer) -- [Customization](#customization) -- [Configuration files](#configuration-files) -- [Troubleshooting](#troubleshooting) -- [Contributing](#contributing) - ---- - -## Why - -Godot's FileSystem dock is handy, but it takes up space permanently. Asset Drawer brings the idea of Unreal's Content Drawer to Godot: a file browser that **appears on demand**, covers the bottom of the editor, then disappears when you no longer need it. It stays connected to the rest of Godot: folder colors, favorites, and drag and drop into the scene or the Inspector. - -## Installation - -1. Copy the `asset_drawer` folder into your project's `res://addons/` folder: - ``` - res://addons/asset_drawer/ - ├── plugin.cfg - ├── plugin.gd - └── drawer.gd - ``` -2. Open **Project → Project Settings → Plugins** and enable **Asset Drawer**. -3. Open the drawer with **Ctrl+Space**, with the toolbar button, or from **Project → Tools → Asset Drawer**. - -> Compatible with **Godot 4.2 and later**. - -## Quick start - -| I want to… | I do… | -|---|---| -| Open / close the drawer | `Ctrl+Space` | -| Keep the drawer open | Click the pin in the top bar | -| Resize the height | Drag the handle at the top of the drawer | -| Add an asset to the scene | Drag it from the drawer into the scene | -| Assign a resource in the Inspector | Drag it onto the matching field | -| Find a file | `Ctrl+F`, then type the name | - -By default, clicking outside the drawer closes it, unless it is pinned. - -## Shortcuts - -| Shortcut | Action | -|---|---| -| `Ctrl+Space` | Open / close the drawer | -| `Esc` | Close the drawer (unless pinned) or the context menu | -| `Ctrl+F` | Focus the search box | -| `F2` | Rename | -| `Ctrl+D` | Duplicate | -| `Delete` | Move to trash (with confirmation) | -| `Backspace` | Parent folder | -| `Enter` | Open the selected item | -| Double-click | Open (scene, script, resource, folder) | -| `Ctrl` + mouse wheel | Thumbnail zoom | -| Mouse back / forward buttons | Back / forward history | -| `Alt+V` | Paste the copied resource onto the hovered Inspector field | - -The open shortcut leaves script autocompletion alone when a `TextEdit` / `CodeEdit` has focus. - -## Features - -### Interface - -- Floating drawer with **margins, rounded corners, an accent outline and a shadow** so it stands out from the editor -- Slide-up opening animation (slight bounce and glow on the outline), animated closing -- Resizable height, remembered between sessions -- **Pinned** mode: the drawer stays open -- **Details** panel with preview (type, size, date, UID), can be hidden -- Status bar (item count, selection, path) -- Animated notifications (green = success, red = error) to confirm actions -- Micro-animations on buttons, rounded selection and hover in the list - -### Navigation - -- **Folder tree** with parent folders pinned to the top as you go deeper, single click to open / expand, "Collapse all" and "Locate current folder" buttons -- Clickable **breadcrumb** and back / forward history -- Side tabs: **Folders**, **Favorites**, **Recents**, **Sets** -- Favorites synced with Godot's FileSystem dock - -### Search and display - -- Recursive search inside the current folder (debounced) -- Type filters: All, Scenes, Scripts, Models, Images, Audio, Shaders -- **Grid** or **list** view, with a separate zoom level remembered for each view -- HD thumbnails generated asynchronously by Godot, loaded only for visible items -- Cap on displayed items (`MAX_ITEMS`) to stay smooth in large folders - -### Folder colors - -- Uses the **custom colors from Godot's FileSystem dock** (the `file_customization/folder_colors` setting) -- Subfolders **inherit** their parent's color, like in the FileSystem dock -- Editable straight from the drawer: right-click a folder, then pick from the color swatches - -### File management - -- New folder, new scene, new script (unique default name, never overwrites) -- Rename: the matching `.import` / `.uid` files follow, along with favorites, sets and colors -- Duplicate files and folders, including recursively -- Delete to the OS **trash**, with confirmation -- Reimport -- Copy path, absolute path or UID -- Show in the FileSystem dock or in the OS file manager - -### Sets - -Named collections of assets, like Unreal's Collections: group files from different folders into one set and find them in the **Sets** tab. Right-click a set to delete it (the files are not touched). - -### Drag and drop - -Drag one or more assets into the scene, the scene tree or Inspector fields, just like from the FileSystem dock. - -## Right-click menu - -**On a file or folder**: open, show in dock / file manager, copy path / absolute path / UID, copy resource, new folder / scene / script, rename, duplicate, favorites, add to a set, reimport, folder color, delete. - -**On empty space** (current folder options): - -- Open the folder in the file manager -- Show in the FileSystem dock -- Copy the folder path / absolute path -- Add / remove the folder from favorites -- Refresh -- Switch to grid or list view -- Export an image of the drawer (transparent PNG or green background) -- New folder / scene / script -- Folder color - -## Copy / paste a resource - -1. Hover a resource in the drawer: a small green **Copy** button appears (or right-click → "Copy resource"). -2. Hover a resource field in the Inspector (for example the `Mesh` of a `MeshInstance3D`): a **Paste** button appears. -3. Click it, or use `Alt+V`. The drawer does not need to be open to paste. - -## Export an image of the drawer - -Handy for posters, renders or your own README. Right-click on empty space in the drawer: - -- **Export image (transparent PNG)**: the drawer is captured on black, then on white, and the opacity of each pixel is computed from the difference. Rounded corners and shadow come out clean, with no cutting out needed. It takes a few seconds to process. -- **Export image (green background)**: the drawer on pure green (0, 255, 0), to key out with a chroma key. - -The image is saved to your Pictures folder and the file manager opens on it. Floating widgets (Copy / Paste buttons, notifications, hint pill) are hidden during the capture. - -## Customization - -Appearance settings are constants at the top of `drawer.gd`: - -| Constant | Purpose | Default | -|---|---|---| -| `HEIGHT_RATIO` | Default height (share of the editor window) | `0.19` | -| `MIN_HEIGHT` | Minimum height (px) | `140` | -| `SIDE_GAP` | Space on each side (px) | `40` | -| `BOTTOM_GAP` | Space below the drawer (px) | `10` | -| `MAX_WIDTH` | Maximum width (px) | `2200` | -| `ANIM_TIME` | Animation duration (s) | `0.2` | -| `SHOW_HINT` | "Asset Drawer Ctrl+Space" pill at the bottom of the editor | `true` | -| `MAX_ITEMS` | Maximum displayed items | `3000` | -| `SEARCH_DELAY` | Search debounce (s) | `0.18` | -| `STICKY_MAX` | Maximum pinned parent folders in the tree | `3` | - -Colors (`COLOR_*`, `TEXT_*`) and the folder palette (`FOLDER_COLORS`) are in the same file. - -## Configuration files - -- The drawer state (current folder, height, view, zoom, pinning, favorites, recents, sets) is saved in `res://.godot/asset_drawer.cfg`. The `.godot/` folder is ignored by Git by default. -- Folder colors are stored by Godot in `project.godot`, under `file_customization/folder_colors`. - -## Troubleshooting - -**Thumbnails don't show up**: wait for the project import to finish, then right-click on empty space → **Refresh**. - -**Ctrl+Space does nothing**: check that the plugin is enabled and that the shortcut isn't already taken by your system (some operating systems use it to switch keyboard language). The toolbar button and the **Project → Tools** menu work in any case. - -**A folder color doesn't show**: colors come from the FileSystem dock. Set it there, or through the drawer's right-click menu, and it will be visible everywhere. - -**An error at startup**: disable then re-enable the plugin, and check that the three files are present in `res://addons/asset_drawer/`. diff --git a/addons/asset_drawer/drawer.gd b/addons/asset_drawer/drawer.gd deleted file mode 100644 index 49e83a5..0000000 --- a/addons/asset_drawer/drawer.gd +++ /dev/null @@ -1,4987 +0,0 @@ -@tool -extends PanelContainer - -const HEIGHT_RATIO := 0.15 # drawer height (share of the editor window) -const MIN_HEIGHT := 60.0 # the content imposes a minimum anyway (top / bottom bars) -const CFG_VERSION := 4 # v3: more compact drawer by default (reduced height) -const SIDE_GAP := 40.0 # gap left on each side -const BOTTOM_GAP := 40.0 # gap below the drawer (keeps the Output / Debugger / Audio bar visible...) -const MAX_WIDTH := 2200.0 -const DETAILS_MIN_WIDTH := 140.0 # minimum width of the details panel (resizable beyond it) -const ANIM_TIME := 0.2 -const SHOW_HINT := true # "Asset Drawer Ctrl+Space" pill at the bottom of the editor -const HINT_BOTTOM := 44.0 -const MAX_ITEMS := 3000 # cap on displayed items (performance) -const SEARCH_DELAY := 0.18 # search debounce -const SAVE_DELAY := 0.6 # config save debounce -const PREVIEW_DELAY := 0.08 # thumbnail loading debounce -const STICKY_MAX := 3 # max number of parent folders pinned at the top of the tree -# _sticky_row_h() = height reserved per pinned row (always equal to one full row). -const ICON_BUCKETS := [24, 64, 96, 160] -const HIDDEN_ALPHA := 0.45 # opacity of hidden items when they are shown -const CFG_PATH := "res://.godot/asset_drawer.cfg" -const L := preload("res://addons/asset_drawer/lang.gd") # FR / EN translations - -# --- Palette -------------------------------------------------------------- -# The background / border / text colors below are VARIABLES: the values written here -# are only a fallback. They are recomputed at startup by _update_palette() from the -# editor theme (Editor Settings > Interface > Theme > Base Color + Contrast). -# Principle: few hues, a clear elevation hierarchy (from the darkest "foundation" -# to the lightest "under the fingers"), an accent used sparingly (drawer border, -# selection, focus) rather than spread everywhere, and a single gray scale for text. -var COLOR_BG_MAIN := Color(0.045, 0.05, 0.067, 1.0) # foundation (the drawer itself) -var COLOR_HEADER := Color(0.083, 0.097, 0.14, 1.0) # top bar (nav + search + filters) -var COLOR_LEFT_PANEL := Color(0.083, 0.097, 0.14, 1.0) # folder tree -var COLOR_CENTER_PANEL := Color(0.083, 0.097, 0.14, 1.0) # asset grid/list, slightly raised -var COLOR_RIGHT_PANEL := Color(0.083, 0.097, 0.14, 1.0) # details panel -var COLOR_FOOTER := Color(0.084, 0.091, 0.114) # status bar, the darkest (bottom anchor) -var COLOR_BORDER := Color(0.26, 0.30, 0.37, 0.65) # thin edge between panels, not a hard line -var COLOR_PATH_BG := Color(0.062, 0.068, 0.086) # shared background of the top bar containers -const BAR_H := 26.0 # shared height of the top bar containers - -# Text hierarchy: a single gray scale (never pure white, easier on the eyes). -var TEXT_PRIMARY := Color(0.93, 0.94, 0.97) # titles, name of the selected file -var TEXT_SECONDARY := Color(0.93, 0.94, 0.97, 0.62) # paths, meta info, status text -var TEXT_MUTED := Color(0.93, 0.94, 0.97, 0.38) # subtle text (placeholders, separators) -var COLOR_POPUP_BG := Color(0.09, 0.10, 0.13, 0.97) # notification background -var COLOR_CARD_BG := Color(0.10, 0.12, 0.15, 0.96) # background of small cards (drag preview, drop target) -var COLOR_TRACK := Color(0.42, 0.46, 0.54) # track of the zoom slider -var _font_color := Color(0.93, 0.94, 0.97) # editor text color (base of the _ov overlays) -var _palette_sig := "" # signature of the theme used to build the UI - -# Semantic colors: each one has a single meaning across the whole UI. -const COLOR_FAV := Color(0.98, 0.78, 0.32) # favorites (gold) -const COLOR_RES_ACTION := Color(0.34, 0.76, 0.48) # copy/paste a resource (green) -const COLOR_DANGER := Color(0.90, 0.36, 0.40) # deletion, errors (red) - -# Color names = the ones stored by Godot's FileSystem dock -const FOLDER_COLORS := { - "red": Color(0.95, 0.35, 0.35), "orange": Color(0.98, 0.58, 0.25), - "yellow": Color(0.96, 0.80, 0.25), "green": Color(0.35, 0.78, 0.45), - "teal": Color(0.25, 0.78, 0.75), "cyan": Color(0.35, 0.75, 0.95), - "blue": Color(0.35, 0.65, 0.98), "purple": Color(0.68, 0.45, 0.95), - "pink": Color(0.95, 0.45, 0.72), "gray": Color(0.60, 0.62, 0.68) -} -const DEFAULT_FOLDER_COLOR := Color(0.35, 0.65, 0.98) - -# Color strip under each thumbnail (in list view: bar on the left), one color per asset type, -# like in Unreal's Content Browser. Set SHOW_TYPE_BAR to false to disable it. -const SHOW_TYPE_BAR := true -const TYPE_COLORS := { - "scene": Color(0.30, 0.58, 0.98), # .tscn / .scn scenes — blue - "script": Color(0.35, 0.78, 0.45), # scripts — green - "model": Color(0.68, 0.45, 0.95), # 3D models and meshes — purple - "image": Color(0.98, 0.50, 0.16), # textures / images — bright orange - "audio": Color(0.95, 0.80, 0.18), # sounds — yellow/gold, clearly distinct from the orange above - "shader": Color(0.95, 0.45, 0.72), # shaders — pink - "material": Color(0.20, 0.80, 0.55), # materials — teal - "animation": Color(0.35, 0.68, 0.98), # animations — sky blue, distinct from the teal above - "font": Color(0.85, 0.72, 0.55), # fonts — beige - "resource": Color(0.60, 0.62, 0.68), # any other resource / file — gray -} - -var is_open := false -var pinned := false -var current_dir := "res://" -var _history: PackedStringArray = PackedStringArray(["res://"]) -var _history_i := 0 -var _favorites: PackedStringArray = PackedStringArray() -var _fav_migrated := false # old local favorites already merged into Godot's -var _recents: PackedStringArray = PackedStringArray() -var _sets: Dictionary = {} # set name -> Array of paths -var _active_set := "" -var _collapsed: Dictionary = {} -var _tween: Tween -var _glow_tween: Tween -var _panel_style: StyleBoxFlat -var _toast: PanelContainer -var _toast_overlay: Control -var _toast_lbl: Label -var _toast_tween: Tween -var _height_ratio := HEIGHT_RATIO -var _resizing := false -var _view_list := false -var _details_visible := true -var _context_path := "" -var _clip_resource: Resource -var _clip_resource_name := "" -var _paste_pop: Button -var _paste_target: EditorResourcePicker -var _paste_tween: Tween -var _copy_pop: Button -var _copy_target_path := "" -var _copy_tween: Tween -var _pending_select := "" -var _selected_filter := 0 -var _hidden: PackedStringArray = PackedStringArray() # hidden items (folder = path ending with "/") -var _show_hidden := false -var _search_all := true # search the whole project (otherwise: current folder) -var _last_script_ext := "gd" -var _index: Array = [] # flat index of file/folder names (global search) -var _index_dirty := true -var _btn_scope: Button -var _active_left_tab := 0 -var _syncing_tree := false -var _zoom_grid := 80.0 # remembered grid-mode zoom (thumbnail size) -var _zoom_list := 22.0 # remembered list-mode zoom (icon size) - -var _split: HSplitContainer -var _left_vbox: VBoxContainer -var _tree: Tree -var _tree_tools: HBoxContainer -var _sticky_box: Control -var _tree_vscroll: VScrollBar -var _sticky_sig := "" -var _row_pitch := 0.0 -var _pending_reveal: TreeItem -var _center_tween: Tween -var _tab_list: ItemList -var _btn_new_set: Button -var _breadcrumbs: HBoxContainer -var _search: LineEdit -var _zoom: HSlider -var _list: ItemList -var _empty_state: VBoxContainer -var _empty_lbl: Label -var _empty_link: LinkButton -var _status: Label -var _details_panel: PanelContainer -var _details_width := 190.0 -var _preview: TextureRect -var _preview_shadows: Array[TextureRect] = [] -var _details_name: Label -var _details_path: Label -var _lbl_type: Label -var _lbl_size: Label -var _lbl_modified: Label -var _lbl_uid: Label -var _btn_open: Button -var _btn_fav: Button -var _btn_copy_res: Button -var _btn_copy_path: Button -var _details_scroll: ScrollContainer -var _details_dv: VBoxContainer -var _details_btn_box: BoxContainer -var _details_type_row: HFlowContainer -var _chip_type: PanelContainer -var _chip_size: PanelContainer -var _chip_date: PanelContainer -var _field_path: VBoxContainer -var _field_uid: VBoxContainer -var _details_card: PanelContainer -var _details_sep_info: ColorRect -var _details_card_sep: ColorRect -var _btn_labels: Dictionary = {} # button -> [label, tooltip] -var _details_compact := false # true: buttons on a single row of icon-only buttons -var _btn_grid: Button -var _btn_list: Button -var _btn_details: Button -var _btn_pin: Button -var _filter_buttons: Array[Button] = [] -var _left_tab_buttons: Array[Button] = [] -var _ctx_popup: PanelContainer -var _copy_hiding := false -var _paste_hiding := false -var _ctx_vbox: VBoxContainer -var _grip: ColorRect -var _hd_icon_cache := {} -var _hint: PanelContainer -var _hint_tween: Tween -var _btn_back: Button -var _empty_reset: Button -var _btn_fwd: Button -var _btn_up: Button -var _outside_close_msec := -1000 -var _card_images := {} -var _preview_cache := {} # path -> Godot thumbnail (null = failure) -var _preview_pending := {} # paths already sent to the previewer -var _svg_queue: Array = [] # [path, target size]: SVGs we rasterize ourselves (sharp at any size) -var _svg_px := {} # path -> size (px) of the last successful rasterization -var _svg_fail := {} # SVGs Godot could not rasterize: we fall back to its thumbnail -var _path_index := {} # path -> index in _list -var _item_colors := PackedColorArray() # color of each _list item's strip (transparent = none) -var _bar_style: StyleBoxFlat -var _dir_paths := {} # paths of the displayed folders -var _drop_list_idx := -1 # list folder highlighted during a drag and drop -var _drop_tree_item: TreeItem # tree folder highlighted during a drag and drop -var _drop_whole := false # drop into the current folder (empty area): frame around the list -var _mq_active := false # rectangle selection ("lasso") in progress -var _mq_moved := false -var _mq_start := Vector2.ZERO # start in "content" coordinates (scroll included) -var _mq_cur := Vector2.ZERO # current mouse position (list coordinates) -var _mq_base: PackedInt32Array = PackedInt32Array() # starting selection (Ctrl / Shift) -var _tree_items := {} # normalized path -> TreeItem -var _dirty := true -var _tree_dirty := true -var _save_dirty := false -var _truncated := false -var _icon_bucket := 0 -var _search_timer: Timer -var _save_timer: Timer -var _preview_timer: Timer - - -func _ready() -> void: - visible = false - modulate.a = 0.0 - mouse_filter = Control.MOUSE_FILTER_STOP - z_index = 128 - z_as_relative = false - clip_contents = false - _search_timer = _make_timer(SEARCH_DELAY, _refresh) - _save_timer = _make_timer(SAVE_DELAY, _flush_cfg) - _preview_timer = _make_timer(PREVIEW_DELAY, _request_visible_previews) - _load_cfg() - _update_palette() - _setup_style() - _build_ui() - _apply_saved_state() - - var base := EditorInterface.get_base_control() - if not base.resized.is_connected(_on_editor_resized): - base.resized.connect(_on_editor_resized) - var fs := EditorInterface.get_resource_filesystem() - if not fs.filesystem_changed.is_connected(_on_fs_changed): - fs.filesystem_changed.connect(_on_fs_changed) - if not fs.resources_reimported.is_connected(_on_reimported): - fs.resources_reimported.connect(_on_reimported) - if SHOW_HINT: - _build_hint(base) - - -func _exit_tree() -> void: - _flush_cfg() - if _hint_tween and _hint_tween.is_valid(): - _hint_tween.kill() - if _paste_tween and _paste_tween.is_valid(): - _paste_tween.kill() - if is_instance_valid(_hint): - _hint.queue_free() - _hint = null - if is_instance_valid(_toast_overlay): - _toast_overlay.queue_free() - _toast_overlay = null - var base := EditorInterface.get_base_control() - if is_instance_valid(base) and base.resized.is_connected(_on_editor_resized): - base.resized.disconnect(_on_editor_resized) - var fs := EditorInterface.get_resource_filesystem() - if is_instance_valid(fs): - if fs.filesystem_changed.is_connected(_on_fs_changed): - fs.filesystem_changed.disconnect(_on_fs_changed) - if fs.resources_reimported.is_connected(_on_reimported): - fs.resources_reimported.disconnect(_on_reimported) - - -func _make_timer(delay: float, cb: Callable) -> Timer: - var t := Timer.new() - t.one_shot = true - t.wait_time = delay - t.timeout.connect(cb) - add_child(t) - return t - - -func _on_fs_changed() -> void: - # Nothing to rebuild while the drawer is closed: we just mark it "dirty". - _dirty = true - _tree_dirty = true - _index_dirty = true - if is_open: - _refresh() - - -func _on_reimported(paths: PackedStringArray) -> void: - for p in paths: - _preview_cache.erase(p) - _preview_pending.erase(p) - _svg_px.erase(p) - _svg_fail.erase(p) - - -func _apply_saved_state() -> void: - _configure_zoom() - _btn_grid.button_pressed = not _view_list - _btn_list.button_pressed = _view_list - _btn_pin.button_pressed = pinned - _btn_details.button_pressed = _details_visible - _details_panel.visible = _details_visible - - -## Drawer palette = the editor's. Backgrounds come from "Base Color" and "Contrast" -## (Godot derives dark_color_1/2/3 from them with base.lerp(black, contrast), contrast * 1.5, contrast * 2). -## We read those colors from the editor theme (they also follow the presets), and fall back to -## computing them from the settings if they are missing. -func _update_palette() -> void: - var base := EditorInterface.get_base_control() - var es := EditorInterface.get_editor_settings() - var base_c := Color(0.21, 0.24, 0.29) - var contrast := 0.3 - if es.has_setting("interface/theme/base_color"): - base_c = es.get_setting("interface/theme/base_color") - if es.has_setting("interface/theme/contrast"): - contrast = float(es.get_setting("interface/theme/contrast")) - if base.has_theme_color("base_color", "Editor"): - base_c = base.get_theme_color("base_color", "Editor") - var black := Color(0, 0, 0, 1) - var d1 := base_c.lerp(black, contrast) - var d2 := base_c.lerp(black, contrast * 1.5) - var d3 := base_c.lerp(black, contrast * 2.0) - if base.has_theme_color("dark_color_1", "Editor"): - d1 = base.get_theme_color("dark_color_1", "Editor") - if base.has_theme_color("dark_color_2", "Editor"): - d2 = base.get_theme_color("dark_color_2", "Editor") - if base.has_theme_color("dark_color_3", "Editor"): - d3 = base.get_theme_color("dark_color_3", "Editor") - base_c = _solid(base_c) - d1 = _solid(d1) - d2 = _solid(d2) - d3 = _solid(d3) - - var font := Color(0.93, 0.94, 0.97) if base_c.get_luminance() < 0.5 else Color(0.10, 0.11, 0.13) - if base.has_theme_color("font_color", "Editor"): - font = base.get_theme_color("font_color", "Editor") - font = _solid(font) - - _font_color = font - # Panels use d1 (the least dark): with a high contrast, d2 / d3 become - # almost black. The drawer background and the inset fields use d2, always darker than the panels. - COLOR_BG_MAIN = d2 # foundation (the drawer itself) - COLOR_HEADER = d1 # top bar - COLOR_LEFT_PANEL = d1 # folder tree - COLOR_CENTER_PANEL = d1 # asset grid / list - COLOR_RIGHT_PANEL = d1 # details panel - COLOR_FOOTER = d1 # status bar - COLOR_PATH_BG = d2 # inset containers of the top bar - COLOR_BORDER = _with_a(base_c.lerp(font, 0.22), 0.65) - COLOR_POPUP_BG = _with_a(d1, 0.97) - COLOR_CARD_BG = _with_a(d1, 0.96) - COLOR_TRACK = base_c.lerp(font, 0.35) - TEXT_PRIMARY = font - TEXT_SECONDARY = _with_a(font, 0.62) - TEXT_MUTED = _with_a(font, 0.38) - _palette_sig = _palette_signature() - - -## Fingerprint of the current theme: lets the plugin know whether the drawer must be rebuilt. -func _palette_signature() -> String: - var base := EditorInterface.get_base_control() - var es := EditorInterface.get_editor_settings() - var parts := PackedStringArray() - for k in ["interface/theme/base_color", "interface/theme/contrast", "interface/theme/accent_color"]: - parts.append(str(es.get_setting(k)) if es.has_setting(k) else "-") - for n in ["base_color", "dark_color_1", "dark_color_2", "dark_color_3", "font_color", "accent_color"]: - parts.append(str(base.get_theme_color(n, "Editor")) if base.has_theme_color(n, "Editor") else "-") - return "|".join(parts) - - -func palette_changed() -> bool: - return _palette_signature() != _palette_sig - - -func _solid(c: Color) -> Color: - return Color(clampf(c.r, 0.0, 1.0), clampf(c.g, 0.0, 1.0), clampf(c.b, 0.0, 1.0), 1.0) - - -func _with_a(c: Color, a: float) -> Color: - return Color(c.r, c.g, c.b, a) - - -## Translucent overlay in the editor's text color (hovers, separators...). -## Replaces the old Color(1, 1, 1, a), invisible on a light theme. -func _ov(a: float) -> Color: - return Color(_font_color.r, _font_color.g, _font_color.b, a) - - -func _make_stylebox(bg: Color, border: Color = Color.TRANSPARENT, radius: int = 6, border_w: int = 1) -> StyleBoxFlat: - var style := StyleBoxFlat.new() - style.bg_color = bg - if border != Color.TRANSPARENT: - style.border_color = border - style.set_border_width_all(border_w) - style.set_corner_radius_all(radius) - return style - - -## Lightens a color for a hovered state — a single setting used everywhere (chips, -## list rows, floating buttons) so the hover "feel" is identical -## across the whole drawer rather than slightly different depending on where you look. -func _hover_c(c: Color) -> Color: - return c.lightened(0.09) - - -## Darkens a color for a pressed/active state — counterpart of _hover_c() above. -func _press_c(c: Color) -> Color: - return c.darkened(0.16) - - -func _setup_style() -> void: - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - var style := StyleBoxFlat.new() - style.bg_color = COLOR_BG_MAIN - # Thin, discreet outline (not a saturated solid line): the drawer must read as a - # raised card above the editor, not as a neon-framed window. - style.set_border_width_all(1) - style.border_color = Color(accent.r, accent.g, accent.b, 0.55) - style.set_corner_radius_all(12) - # Soft, wide shadow rather than tight: that is what gives the "floating card" feel - # of depth in pro UIs, as opposed to a hard edge that reads as a "game window". - style.shadow_color = Color(0, 0, 0, 0.30) - style.shadow_size = 12 - style.shadow_offset = Vector2(0, -12) # shifted upward: nothing sticks out below the drawer - add_theme_stylebox_override("panel", style) - _panel_style = style - - -func _pad(margin: int, top_bottom: int = -1) -> MarginContainer: - var m := MarginContainer.new() - var tb := margin if top_bottom < 0 else top_bottom - m.add_theme_constant_override("margin_left", margin) - m.add_theme_constant_override("margin_right", margin) - m.add_theme_constant_override("margin_top", tb) - m.add_theme_constant_override("margin_bottom", tb) - return m - - -func _build_ui() -> void: - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - var root := VBoxContainer.new() - root.add_theme_constant_override("separation", 0) - add_child(root) - - _grip = ColorRect.new() - _grip.custom_minimum_size = Vector2(0, 6) - _grip.color = _ov(0.06) - _grip.mouse_default_cursor_shape = Control.CURSOR_VSIZE - _grip.gui_input.connect(_on_grip_input) - root.add_child(_grip) - - var pad := _pad(6, 2) - pad.size_flags_vertical = Control.SIZE_EXPAND_FILL - root.add_child(pad) - - var main_vbox := VBoxContainer.new() - main_vbox.add_theme_constant_override("separation", 3) - pad.add_child(main_vbox) - - # --- HEADER --- - var header := PanelContainer.new() - header.add_theme_stylebox_override("panel", _make_stylebox(COLOR_HEADER, COLOR_BORDER, 6, 1)) - var header_pad := _pad(5, 2) - header.add_child(header_pad) - header_pad.add_child(_build_toolbar()) - main_vbox.add_child(header) - - # --- SPLIT --- - _split = HSplitContainer.new() - _split.size_flags_vertical = Control.SIZE_EXPAND_FILL - _split.split_offset = 0 - main_vbox.add_child(_split) - - # --- LEFT PANEL --- - var left_panel := VBoxContainer.new() - left_panel.custom_minimum_size = Vector2(180, 0) - left_panel.add_theme_constant_override("separation", 4) - - # Tabs (Folders / Favorites / Recents / Sets) in their own container - var tabs_panel := PanelContainer.new() - tabs_panel.add_theme_stylebox_override("panel", _make_stylebox(COLOR_HEADER, COLOR_BORDER, 6, 1)) - var tabs_pad := _pad(3) - tabs_panel.add_child(tabs_pad) - var left_tabs := HBoxContainer.new() - left_tabs.add_theme_constant_override("separation", 2) - tabs_pad.add_child(left_tabs) - left_panel.add_child(tabs_panel) - - # Tab content: same background for the tree and the lists - var left_content := PanelContainer.new() - left_content.size_flags_vertical = Control.SIZE_EXPAND_FILL - left_content.add_theme_stylebox_override("panel", _make_stylebox(COLOR_LEFT_PANEL, COLOR_BORDER, 6, 1)) - var left_pad := _pad(4) - left_content.add_child(left_pad) - _left_vbox = VBoxContainer.new() - left_pad.add_child(_left_vbox) - left_panel.add_child(left_content) - var tab_group := ButtonGroup.new() - var tab_names := [L.t("Dossiers"), L.t("Favoris"), L.t("Récents"), "Sets"] - for i in range(tab_names.size()): - var btn := Button.new() - btn.text = tab_names[i] - btn.toggle_mode = true - btn.button_group = tab_group - btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _style_ghost_button(btn, 4, 3, Color(accent.r, accent.g, accent.b, 0.45)) - btn.button_pressed = (i == 0) - btn.focus_mode = Control.FOCUS_NONE - btn.add_theme_font_size_override("font_size", 11) - var tab_idx := i - btn.pressed.connect(func() -> void: _switch_left_tab(tab_idx)) - left_tabs.add_child(btn) - _left_tab_buttons.append(btn) - - _tree_tools = HBoxContainer.new() - _tree_tools.add_theme_constant_override("separation", 2) - _left_vbox.add_child(_tree_tools) - _add_tree_tool_button("CollapseTree", "⇤", L.t("Tout replier"), _collapse_all_folders) - _add_tree_tool_button("ExpandTree", "◎", L.t("Localiser le dossier courant"), _sync_tree_selection) - - _tree = Tree.new() - _tree.hide_root = false - _tree.size_flags_vertical = Control.SIZE_EXPAND_FILL - _tree.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) - _tree.item_selected.connect(_on_tree_selected) - _tree.item_collapsed.connect(func(it: TreeItem) -> void: - if _syncing_tree: - return - _collapsed[str(it.get_metadata(0))] = it.collapsed - ) - _tree.gui_input.connect(_on_tree_gui_input) - _tree.set_drag_forwarding(_tree_get_drag, _tree_can_drop, _tree_drop) - _tree.draw.connect(_draw_tree_drop) - _left_vbox.add_child(_tree) - _setup_sticky() - - _tab_list = ItemList.new() - _tab_list.size_flags_vertical = Control.SIZE_EXPAND_FILL - _tab_list.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) - _tab_list.visible = false - _tab_list.item_activated.connect(_on_tab_list_activated) - _tab_list.item_clicked.connect(_on_tab_list_clicked) - _style_item_list(_tab_list) - _left_vbox.add_child(_tab_list) - - _btn_new_set = Button.new() - _btn_new_set.text = L.t("+ Nouveau set") - _btn_new_set.visible = false - _btn_new_set.pressed.connect(func() -> void: - _prompt_string(L.t("Nouveau set"), L.t("mon_set"), func(n: String) -> void: - if _valid_name(n) and not _sets.has(n): - _sets[n] = [] - _save_cfg() - _refresh_left_tab_content() - ) - ) - _left_vbox.add_child(_btn_new_set) - _split.add_child(left_panel) - - # --- CENTER AREA --- - var center_panel := PanelContainer.new() - center_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL - center_panel.add_theme_stylebox_override("panel", _make_stylebox(COLOR_CENTER_PANEL, COLOR_BORDER, 6, 1)) - var center_pad := _pad(4) - center_panel.add_child(center_pad) - - _list = ItemList.new() - _list.size_flags_vertical = Control.SIZE_EXPAND_FILL - _list.icon_mode = ItemList.ICON_MODE_TOP - _list.max_columns = 0 - _list.select_mode = ItemList.SELECT_MULTI - _list.same_column_width = true - _list.max_text_lines = 2 - _list.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) - # Keyboard focus keeps working (arrows, activation...), but without the accent frame - # that Godot draws by default around the focused control — it looked like an unintended - # selection of the whole panel when the drawer opened. - _list.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) - _list.item_activated.connect(_on_item_activated) - _list.draw.connect(_draw_type_bars) - _list.draw.connect(_draw_list_overlay) - _list.multi_selected.connect(func(_i: int, _s: bool) -> void: _on_selection_changed()) - _list.empty_clicked.connect(func(_p: Vector2, _b: int) -> void: - _list.deselect_all() - _on_selection_changed() - ) - _list.set_drag_forwarding(_get_drag_data_fw, _list_can_drop, _list_drop) - _list.gui_input.connect(_on_list_gui_input) - _style_item_list(_list) - _list.resized.connect(_schedule_previews) - _list.get_v_scroll_bar().value_changed.connect(func(_v: float) -> void: _schedule_previews()) - center_pad.add_child(_list) - - _empty_state = VBoxContainer.new() - _empty_state.alignment = BoxContainer.ALIGNMENT_CENTER - _empty_state.mouse_filter = Control.MOUSE_FILTER_IGNORE - _empty_state.visible = false - _empty_lbl = Label.new() - _empty_lbl.text = L.t("Aucun asset") - _empty_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - _empty_lbl.modulate = TEXT_SECONDARY - _empty_state.add_child(_empty_lbl) - _empty_link = LinkButton.new() - _empty_link.text = L.t("Créer un dossier") - _empty_link.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - _empty_link.add_theme_color_override("font_color", accent) - _empty_link.pressed.connect(_create_folder) - _empty_state.add_child(_empty_link) - _empty_reset = Button.new() - _empty_reset.text = L.t("Réinitialiser les filtres") - _empty_reset.visible = false - _empty_reset.focus_mode = Control.FOCUS_NONE - _empty_reset.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - var reset_n := _pill_style(accent) - var reset_h := _pill_style(accent.lightened(0.15)) - for st in [reset_n, reset_h]: - (st as StyleBoxFlat).content_margin_left = 12 - (st as StyleBoxFlat).content_margin_right = 12 - (st as StyleBoxFlat).content_margin_top = 4 - (st as StyleBoxFlat).content_margin_bottom = 4 - _empty_reset.add_theme_stylebox_override("normal", reset_n) - _empty_reset.add_theme_stylebox_override("hover", reset_h) - _empty_reset.add_theme_stylebox_override("pressed", reset_n) - _empty_reset.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) - _empty_reset.add_theme_color_override("font_color", Color.WHITE) - _empty_reset.add_theme_color_override("font_hover_color", Color.WHITE) - _empty_reset.pressed.connect(func() -> void: _set_filter(0)) - _empty_state.add_child(_empty_reset) - center_pad.add_child(_empty_state) - # Center + details: resizable HSplitContainer (2 children, compatible with Godot 4.2+). - var right_split := HSplitContainer.new() - right_split.size_flags_horizontal = Control.SIZE_EXPAND_FILL - right_split.dragged.connect(_on_details_split_dragged) - right_split.add_child(center_panel) - _details_panel = _build_details_panel() - right_split.add_child(_details_panel) - # offset=0 would give the panel (non-expand, second child) its minimum width; - # a negative offset gives it back the remembered width (or the default width on first launch). - right_split.split_offset = -int(maxf(_details_width - DETAILS_MIN_WIDTH, 0.0)) - _split.add_child(right_split) - - # --- FOOTER --- - var footer := PanelContainer.new() - footer.add_theme_stylebox_override("panel", _make_stylebox(COLOR_FOOTER, COLOR_BORDER, 6, 1)) - var footer_pad := _pad(6, 1) - footer.add_child(footer_pad) - footer_pad.add_child(_build_footer()) - main_vbox.add_child(footer) - - # Context menu = a simple PanelContainer placed on the editor overlay (rather than a - # PopupPanel window): no more square black corners behind the rounded edges. - _ctx_popup = PanelContainer.new() - _ctx_popup.visible = false - _ctx_popup.mouse_filter = Control.MOUSE_FILTER_STOP - _ctx_popup.z_index = 350 - var ctx_style := _make_stylebox(COLOR_HEADER, COLOR_BORDER, 8, 1) - ctx_style.shadow_color = Color(0, 0, 0, 0.55) - ctx_style.shadow_size = 10 - ctx_style.shadow_offset = Vector2(0, 3) - _ctx_popup.add_theme_stylebox_override("panel", ctx_style) - var ctx_margin := _pad(8) - _ctx_popup.add_child(ctx_margin) - _ctx_vbox = VBoxContainer.new() - _ctx_vbox.add_theme_constant_override("separation", 2) - ctx_margin.add_child(_ctx_vbox) - - _build_toast() - _toast_overlay.add_child(_ctx_popup) - _build_paste_pop() - - -## Small floating "Paste" button that appears near any resource field -## of the Inspector (compatible with the copied resource) when the mouse hovers it — -## like dragging and dropping assets in Unreal's Content Browser. -func _build_paste_pop() -> void: - var base := EditorInterface.get_base_control() - - _paste_pop = Button.new() - _paste_pop.text = "" - _paste_pop.tooltip_text = L.t("Coller la ressource copiée ici") - _paste_pop.custom_minimum_size = Vector2(22, 22) - _paste_pop.expand_icon = true - _paste_pop.visible = false - _paste_pop.modulate.a = 0.0 - _paste_pop.focus_mode = Control.FOCUS_NONE - _paste_pop.mouse_filter = Control.MOUSE_FILTER_STOP - _paste_pop.z_index = 200 # above the drawer panel itself (z_index = 128) - if base.has_theme_icon("ActionPaste", "EditorIcons"): - _paste_pop.icon = base.get_theme_icon("ActionPaste", "EditorIcons") - elif base.has_theme_icon("ActionCopy", "EditorIcons"): - _paste_pop.icon = base.get_theme_icon("ActionCopy", "EditorIcons") - _paste_pop.add_theme_color_override("icon_normal_color", Color.WHITE) - _paste_pop.add_theme_color_override("icon_hover_color", Color.WHITE) - _paste_pop.add_theme_color_override("icon_pressed_color", Color.WHITE) - _paste_pop.add_theme_stylebox_override("normal", _make_stylebox(Color(COLOR_RES_ACTION.r, COLOR_RES_ACTION.g, COLOR_RES_ACTION.b, 0.95), Color(1, 1, 1, 0.55), 5, 1)) - _paste_pop.add_theme_stylebox_override("hover", _make_stylebox(Color(COLOR_RES_ACTION.r, COLOR_RES_ACTION.g, COLOR_RES_ACTION.b, 1.0), Color(1, 1, 1, 0.85), 5, 1)) - _paste_pop.add_theme_stylebox_override("pressed", _make_stylebox(_press_c(COLOR_RES_ACTION), Color.WHITE, 5, 1)) - _paste_pop.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) - _paste_pop.pressed.connect(_on_paste_pop_pressed) - _add_press_bounce(_paste_pop, 1.12, 0.88) - _toast_overlay.add_child(_paste_pop) - - _build_copy_pop() - - -## Small floating "Copy" button (icon only, same green as "Paste") that appears when -## hovering a resource in the drawer's list — counterpart of the "Paste" button on the Inspector side. -func _build_copy_pop() -> void: - var base := EditorInterface.get_base_control() - _copy_pop = Button.new() - _copy_pop.text = "" - _copy_pop.tooltip_text = L.t("Copier la ressource") - _copy_pop.custom_minimum_size = Vector2(22, 22) - _copy_pop.expand_icon = true - _copy_pop.visible = false - _copy_pop.modulate.a = 0.0 - _copy_pop.focus_mode = Control.FOCUS_NONE - _copy_pop.mouse_filter = Control.MOUSE_FILTER_STOP - _copy_pop.z_index = 200 # above the drawer panel itself (z_index = 128) - if base.has_theme_icon("ActionCopy", "EditorIcons"): - _copy_pop.icon = base.get_theme_icon("ActionCopy", "EditorIcons") - elif base.has_theme_icon("Duplicate", "EditorIcons"): - _copy_pop.icon = base.get_theme_icon("Duplicate", "EditorIcons") - _copy_pop.add_theme_color_override("icon_normal_color", Color.WHITE) - _copy_pop.add_theme_color_override("icon_hover_color", Color.WHITE) - _copy_pop.add_theme_color_override("icon_pressed_color", Color.WHITE) - _copy_pop.add_theme_stylebox_override("normal", _make_stylebox(Color(COLOR_RES_ACTION.r, COLOR_RES_ACTION.g, COLOR_RES_ACTION.b, 0.95), Color(1, 1, 1, 0.55), 5, 1)) - _copy_pop.add_theme_stylebox_override("hover", _make_stylebox(Color(COLOR_RES_ACTION.r, COLOR_RES_ACTION.g, COLOR_RES_ACTION.b, 1.0), Color(1, 1, 1, 0.85), 5, 1)) - _copy_pop.add_theme_stylebox_override("pressed", _make_stylebox(_press_c(COLOR_RES_ACTION), Color.WHITE, 5, 1)) - _copy_pop.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) - _copy_pop.pressed.connect(_on_copy_pop_pressed) - _add_press_bounce(_copy_pop, 1.12, 0.88) - _toast_overlay.add_child(_copy_pop) - - -func _build_toast() -> void: - var base := EditorInterface.get_base_control() - # Added at the editor level (like the hint pill), not as a child of the drawer: - # it must stay visible even when the drawer is closed (e.g. after an Alt+V paste). - _toast_overlay = Control.new() - _toast_overlay.name = "AssetDrawerToastOverlay" - _toast_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE - _toast_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) - base.add_child(_toast_overlay) - - # Strip centered at the bottom of the editor that hosts the toast (auto-centered by the CenterContainer). - var bottom_bar := CenterContainer.new() - bottom_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE - bottom_bar.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) - bottom_bar.offset_top = -58 - bottom_bar.offset_bottom = -14 - _toast_overlay.add_child(bottom_bar) - - _toast = PanelContainer.new() - _toast.mouse_filter = Control.MOUSE_FILTER_IGNORE - _toast.modulate.a = 0.0 - # Must stay readable even above the "Ctrl+Space" pill (z_index = 200) and the - # drawer panel (z_index = 128), since both occupy the same area at the bottom of the screen. - _toast.z_index = 300 - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - _toast.add_theme_stylebox_override("panel", _make_stylebox(COLOR_POPUP_BG, Color(accent.r, accent.g, accent.b, 0.85), 8, 1)) - var pad := _pad(14, 7) - _toast.add_child(pad) - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 6) - pad.add_child(row) - _toast_lbl = Label.new() - _toast_lbl.add_theme_font_size_override("font_size", 12) - row.add_child(_toast_lbl) - bottom_bar.add_child(_toast) - - -func _build_toolbar() -> Control: - var wrap := VBoxContainer.new() - wrap.add_theme_constant_override("separation", 2) - - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 5) - wrap.add_child(row) - - # Top bar: each group in a dark container of the same height, same outline, same corners - var accent_bar: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - - # 1) Navigation - var nav_row := HBoxContainer.new() - nav_row.add_theme_constant_override("separation", 2) - row.add_child(_bar_panel(nav_row)) - - _btn_back = _icon_btn("Back", "<") - _btn_back.tooltip_text = L.t("Précédent (bouton souris 4)") - _btn_back.pressed.connect(func() -> void: _hist_go(-1)) - nav_row.add_child(_btn_back) - _btn_fwd = _icon_btn("Forward", ">") - _btn_fwd.tooltip_text = L.t("Suivant (bouton souris 5)") - _btn_fwd.pressed.connect(func() -> void: _hist_go(1)) - nav_row.add_child(_btn_fwd) - _btn_up = _icon_btn("ArrowUp", "^") - _btn_up.tooltip_text = L.t("Dossier parent (Retour arrière)") - _btn_up.pressed.connect(_go_up) - nav_row.add_child(_btn_up) - - # 2) Breadcrumb - _breadcrumbs = HBoxContainer.new() - _breadcrumbs.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _breadcrumbs.clip_contents = true - row.add_child(_bar_panel(_breadcrumbs, true)) - - # 3) Search: same background / outline as the containers - _search = LineEdit.new() - _search.placeholder_text = L.t("Rechercher... (Ctrl+F)") - _search.clear_button_enabled = true - _search.right_icon = EditorInterface.get_base_control().get_theme_icon("Search", "EditorIcons") - _search.custom_minimum_size = Vector2(170, BAR_H) - var search_box := _topbar_stylebox() - search_box.content_margin_left = 8 - search_box.content_margin_right = 6 - search_box.content_margin_top = 2 - search_box.content_margin_bottom = 2 - var search_focus := search_box.duplicate() as StyleBoxFlat - search_focus.border_color = Color(accent_bar.r, accent_bar.g, accent_bar.b, 0.9) - _search.add_theme_stylebox_override("normal", search_box) - _search.add_theme_stylebox_override("focus", search_focus) - _search.add_theme_stylebox_override("read_only", search_box) - _search.text_changed.connect(func(_t: String) -> void: _search_timer.start()) - row.add_child(_search) - - # Search scope: whole project / current folder - _btn_scope = _icon_btn("Filesystem,Folder", L.t("Projet")) - _btn_scope.toggle_mode = true - _btn_scope.button_pressed = _search_all - _btn_scope.toggled.connect(_on_scope_toggled) - _style_ghost_button(_btn_scope, 5, 2, Color(accent_bar.r, accent_bar.g, accent_bar.b, 0.45)) - row.add_child(_bar_panel(_btn_scope)) - _update_search_ui() - - # 4) Display mode (grid / list) - var view_box := HBoxContainer.new() - view_box.add_theme_constant_override("separation", 2) - row.add_child(_bar_panel(view_box)) - var view_group := ButtonGroup.new() - _btn_grid = _icon_btn("FileThumbnail,Grid,TileMap", L.t("Grille")) - _btn_grid.tooltip_text = L.t("Vue grille") - _btn_grid.toggle_mode = true - _btn_grid.button_group = view_group - _btn_grid.pressed.connect(func() -> void: - _view_list = false - _configure_zoom() - _save_cfg() - _refresh() - ) - view_box.add_child(_btn_grid) - _btn_list = _icon_btn("FileList", L.t("Liste")) - _btn_list.tooltip_text = L.t("Vue liste") - _btn_list.toggle_mode = true - _btn_list.button_group = view_group - _btn_list.pressed.connect(func() -> void: - _view_list = true - _configure_zoom() - _save_cfg() - _refresh() - ) - view_box.add_child(_btn_list) - _style_ghost_button(_btn_grid, 5, 2, Color(accent_bar.r, accent_bar.g, accent_bar.b, 0.45)) - _style_ghost_button(_btn_list, 5, 2, Color(accent_bar.r, accent_bar.g, accent_bar.b, 0.45)) - - # 5) Zoom - _zoom = HSlider.new() - _zoom.min_value = 48 - _zoom.max_value = 160 - _zoom.step = 8 - _zoom.value = 80 - _zoom.tooltip_text = L.t("Taille des miniatures (Ctrl+molette)") - _zoom.custom_minimum_size.x = 80 - _zoom.size_flags_vertical = Control.SIZE_SHRINK_CENTER - _zoom.value_changed.connect(_on_zoom_changed) - _style_zoom_slider(accent_bar) - row.add_child(_bar_panel(_zoom, false, 8)) - - # 6) Actions: details, pin, dock, close - var actions := HBoxContainer.new() - actions.add_theme_constant_override("separation", 2) - row.add_child(_bar_panel(actions)) - - _btn_details = _icon_btn("ControlLayout", L.t("Détails")) - _btn_details.tooltip_text = L.t("Panneau de détails") - _btn_details.toggle_mode = true - _btn_details.toggled.connect(func(on: bool) -> void: - _details_visible = on - _details_panel.visible = on - _save_cfg() - ) - _style_ghost_button(_btn_details, 5, 2, Color(accent_bar.r, accent_bar.g, accent_bar.b, 0.45)) - actions.add_child(_btn_details) - - _btn_pin = _icon_btn("Pin", L.t("Épingler")) - _btn_pin.tooltip_text = L.t("Épingler (reste ouvert)") - _btn_pin.toggle_mode = true - _btn_pin.toggled.connect(func(on: bool) -> void: - pinned = on - _save_cfg() - ) - _style_ghost_button(_btn_pin, 5, 2, Color(accent_bar.r, accent_bar.g, accent_bar.b, 0.45)) - actions.add_child(_btn_pin) - - var dock_btn := Button.new() - dock_btn.text = "DOCK" - dock_btn.tooltip_text = L.t("Afficher ce dossier dans le dock Fichiers") - dock_btn.focus_mode = Control.FOCUS_NONE - dock_btn.add_theme_font_size_override("font_size", 10) - _style_ghost_button(dock_btn, 6, 2) - dock_btn.pressed.connect(func() -> void: _show_in_dock(current_dir)) - actions.add_child(dock_btn) - - var close_btn := _icon_btn("Close", "X") - close_btn.tooltip_text = L.t("Fermer (Échap)") - close_btn.pressed.connect(close) - actions.add_child(close_btn) - - # Row 2: filters - var filters_box := HBoxContainer.new() - filters_box.add_theme_constant_override("separation", 3) - var filters := [L.t("Tout"), L.t("Scènes"), "Scripts", L.t("Modèles"), "Images", "Audio", "Shaders"] - for i in range(filters.size()): - var btn := Button.new() - btn.text = filters[i] - btn.focus_mode = Control.FOCUS_NONE - btn.add_theme_font_size_override("font_size", 11) - var idx := i - btn.pressed.connect(func() -> void: _set_filter(idx)) - _add_press_bounce(btn, 1.06, 0.92) - _filter_buttons.append(btn) - filters_box.add_child(btn) - wrap.add_child(filters_box) - _update_filter_pills() - return wrap - - -func _build_details_panel() -> PanelContainer: - var panel := PanelContainer.new() - panel.custom_minimum_size = Vector2(DETAILS_MIN_WIDTH, 0) - panel.add_theme_stylebox_override("panel", _make_stylebox(COLOR_RIGHT_PANEL, COLOR_BORDER, 6, 1)) - - # Everything is in a ScrollContainer: the panel can be very short, so it scrolls. - # When there is no more room, the buttons switch to a single row of icon-only buttons. - _details_scroll = ScrollContainer.new() - _details_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - _details_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO - _details_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _details_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - _details_scroll.resized.connect(_update_details_layout) - panel.add_child(_details_scroll) - var margin := _pad(5) - margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL - margin.size_flags_vertical = Control.SIZE_EXPAND_FILL - _details_scroll.add_child(margin) - var dv := VBoxContainer.new() - dv.add_theme_constant_override("separation", 3) - margin.add_child(dv) - _details_dv = dv - - # --- Header: framed thumbnail on the left; on the right the name then chips (type, size, date) --- - var head := HBoxContainer.new() - head.add_theme_constant_override("separation", 6) - dv.add_child(head) - - # Frameless thumbnail: a small drop shadow is drawn behind the image itself - # (slightly enlarged black silhouettes that get more and more transparent = soft shadow). - var thumb_pad := _pad(4) - thumb_pad.size_flags_vertical = Control.SIZE_SHRINK_BEGIN - head.add_child(thumb_pad) - var thumb_stack := Control.new() - thumb_stack.custom_minimum_size = Vector2(44, 44) - thumb_pad.add_child(thumb_stack) - for layer in [[3.0, 0.07], [2.0, 0.09], [1.0, 0.12]]: - var e: float = layer[0] - var sh := TextureRect.new() - sh.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - sh.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - sh.mouse_filter = Control.MOUSE_FILTER_IGNORE - sh.self_modulate = Color(0, 0, 0, layer[1]) - sh.anchor_right = 1.0 - sh.anchor_bottom = 1.0 - sh.offset_left = -e - sh.offset_right = e - sh.offset_top = -e + 2.0 - sh.offset_bottom = e + 2.0 - thumb_stack.add_child(sh) - _preview_shadows.append(sh) - _preview = TextureRect.new() - _preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - _preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - _preview.anchor_right = 1.0 - _preview.anchor_bottom = 1.0 - thumb_stack.add_child(_preview) - - var head_txt := VBoxContainer.new() - head_txt.size_flags_horizontal = Control.SIZE_EXPAND_FILL - head_txt.size_flags_vertical = Control.SIZE_SHRINK_CENTER - head_txt.add_theme_constant_override("separation", 1) - head.add_child(head_txt) - - _details_name = Label.new() - _details_name.add_theme_font_override("font", EditorInterface.get_base_control().get_theme_font("bold", "EditorFonts")) - _details_name.add_theme_font_size_override("font_size", 12) - _details_name.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _details_name.size_flags_horizontal = Control.SIZE_EXPAND_FILL - head_txt.add_child(_details_name) - - var accent_h: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - _details_type_row = HFlowContainer.new() - _details_type_row.add_theme_constant_override("h_separation", 2) - _details_type_row.add_theme_constant_override("v_separation", 1) - head_txt.add_child(_details_type_row) - _lbl_type = Label.new() - _lbl_size = Label.new() - _lbl_modified = Label.new() - _chip_type = _make_chip(_lbl_type, Color(accent_h.r, accent_h.g, accent_h.b, 0.25)) - _chip_size = _make_chip(_lbl_size, _ov(0.09)) - _chip_date = _make_chip(_lbl_modified, _ov(0.09)) - _details_type_row.add_child(_chip_type) - _details_type_row.add_child(_chip_size) - _details_type_row.add_child(_chip_date) - - # --- Separator + inset info card: path, UID (hidden if there is nothing to show) --- - _details_sep_info = _detail_sep() - dv.add_child(_details_sep_info) - - _details_card = PanelContainer.new() - _details_card.add_theme_stylebox_override("panel", _make_stylebox(COLOR_PATH_BG, COLOR_BORDER.darkened(0.25), 6, 1)) - dv.add_child(_details_card) - var card_pad := _pad(6, 3) - _details_card.add_child(card_pad) - var card_box := VBoxContainer.new() - card_box.add_theme_constant_override("separation", 3) - card_pad.add_child(card_box) - - _details_path = Label.new() - _field_path = _make_field(L.t("Chemin"), _details_path) - card_box.add_child(_field_path) - - _details_card_sep = _detail_sep() - card_box.add_child(_details_card_sep) - - _lbl_uid = Label.new() - _field_uid = _make_field("UID", _lbl_uid) - card_box.add_child(_field_uid) - - var spacer := Control.new() - spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL - dv.add_child(spacer) - - # --- Separator before the actions area --- - dv.add_child(_detail_sep()) - - # --- Buttons: column with text + icon, or a row of icon-only buttons when space is short --- - _details_btn_box = BoxContainer.new() - _details_btn_box.vertical = true - _details_btn_box.add_theme_constant_override("separation", 3) - dv.add_child(_details_btn_box) - _btn_labels.clear() - - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - - _btn_open = _detail_btn("Load,Folder", L.t("Ouvrir")) - _btn_open.add_theme_stylebox_override("normal", _make_stylebox(accent, Color.TRANSPARENT, 4)) - _btn_open.add_theme_stylebox_override("hover", _make_stylebox(_hover_c(accent), Color.TRANSPARENT, 4)) - _btn_open.add_theme_stylebox_override("pressed", _make_stylebox(_press_c(accent), Color.TRANSPARENT, 4)) - _tint_btn_icon(_btn_open, Color.WHITE) - _btn_open.pressed.connect(func() -> void: _open_path(_context_path)) - _details_btn_box.add_child(_btn_open) - - _btn_copy_res = _detail_btn("ActionCopy,Duplicate", L.t("Copier la ressource"), - L.t("Puis survolez un champ ressource de l'Inspecteur (ex. Mesh d'un MeshInstance3D) : un bouton « Coller » apparaît (ou Alt+V au clavier)")) - # Same green as the "Paste resource" button (COLOR_RES_ACTION): the two actions - # form a visual pair (copy here → paste over there). - _btn_copy_res.add_theme_stylebox_override("normal", _make_stylebox(COLOR_RES_ACTION, Color.TRANSPARENT, 4)) - _btn_copy_res.add_theme_stylebox_override("hover", _make_stylebox(_hover_c(COLOR_RES_ACTION), Color(1, 1, 1, 0.55), 4, 1)) - _btn_copy_res.add_theme_stylebox_override("pressed", _make_stylebox(_press_c(COLOR_RES_ACTION), Color.WHITE, 4, 1)) - _btn_copy_res.add_theme_color_override("font_color", Color.WHITE) - _btn_copy_res.add_theme_color_override("font_hover_color", Color.WHITE) - _btn_copy_res.add_theme_color_override("font_pressed_color", Color.WHITE) - _tint_btn_icon(_btn_copy_res, Color.WHITE) - _btn_copy_res.resized.connect(func() -> void: _btn_copy_res.pivot_offset = _btn_copy_res.size * 0.5) - _btn_copy_res.pressed.connect(func() -> void: - _copy_resource_to_clipboard(_context_path) - _bounce_to(_btn_copy_res, Vector2(1.08, 1.08), 0.08) - get_tree().create_timer(0.08).timeout.connect(func() -> void: _bounce_to(_btn_copy_res, Vector2.ONE, 0.14)) - ) - _details_btn_box.add_child(_btn_copy_res) - - _btn_copy_path = _detail_btn("CopyNodePath,ActionCopy,Link", L.t("Copier le chemin")) - _btn_copy_path.pressed.connect(func() -> void: - DisplayServer.clipboard_set(_context_path) - _flash(L.t("Chemin copié")) - ) - _details_btn_box.add_child(_btn_copy_path) - - _btn_fav = _detail_btn("NonFavorite,Favorites", L.t("Ajouter aux favoris")) - _btn_fav.resized.connect(func() -> void: _btn_fav.pivot_offset = _btn_fav.size * 0.5) - _btn_fav.pressed.connect(func() -> void: - var was_fav := _is_fav(_context_path) - _toggle_favorite(_context_path) - _update_details(_context_path) - _refresh() - _bounce_to(_btn_fav, Vector2(1.15, 1.15), 0.1) - get_tree().create_timer(0.1).timeout.connect(func() -> void: _bounce_to(_btn_fav, Vector2.ONE, 0.14)) - _flash(L.t("Retiré des favoris") if was_fav else L.t("Ajouté aux favoris")) - ) - _details_btn_box.add_child(_btn_fav) - _sync_details_meta() - return panel - - -## Rounded chip (type, size, date) around a Label. -func _make_chip(lbl: Label, tint: Color) -> PanelContainer: - var chip := PanelContainer.new() - chip.add_theme_stylebox_override("panel", _make_stylebox(tint, Color.TRANSPARENT, 4)) - var pad := _pad(2, 0) - # Negative vertical margins: removes the line spacing the Label adds around the text - pad.add_theme_constant_override("margin_top", -2) - pad.add_theme_constant_override("margin_bottom", -2) - chip.add_child(pad) - lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - lbl.add_theme_font_size_override("font_size", 9) - lbl.add_theme_constant_override("line_spacing", 0) - lbl.add_theme_constant_override("outline_size", 0) - pad.add_child(lbl) - return chip - - -## "Discreet title above, value below" field: readable even for long values. -func _make_field(title: String, val: Label) -> VBoxContainer: - var f := VBoxContainer.new() - f.add_theme_constant_override("separation", 1) - var t := Label.new() - t.text = title - t.modulate = TEXT_MUTED - t.add_theme_font_size_override("font_size", 9) - f.add_child(t) - val.add_theme_font_size_override("font_size", 10) - val.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - val.size_flags_horizontal = Control.SIZE_EXPAND_FILL - f.add_child(val) - return f - - -## Adds invisible break points (zero-width space) after _ . / - -## so that long names wrap cleanly instead of in the middle of a word. -func _soft_wrap(t: String) -> String: - var zw := char(0x200B) - return t.replace("_", "_" + zw).replace(".", "." + zw).replace("/", "/" + zw).replace("-", "-" + zw) - - -## Thin separator line between the zones of the details panel. -func _detail_sep() -> ColorRect: - var r := ColorRect.new() - r.custom_minimum_size = Vector2(0, 1) - r.color = COLOR_BORDER - r.mouse_filter = Control.MOUSE_FILTER_IGNORE - return r - - -## Details panel button: icon (first name found in the list) + label + tooltip. -func _detail_btn(icon_names: String, label: String, tip: String = "") -> Button: - var b := Button.new() - b.size_flags_horizontal = Control.SIZE_EXPAND_FILL - b.expand_icon = false - b.add_theme_font_size_override("font_size", 11) - b.icon = _icon_from(icon_names) - _btn_labels[b] = [label, tip] - _apply_btn_mode(b) - return b - - -func _icon_from(names: String) -> Texture2D: - var base := EditorInterface.get_base_control() - for n in names.split(","): - if base.has_theme_icon(n, "EditorIcons"): - return base.get_theme_icon(n, "EditorIcons") - return null - - -func _tint_btn_icon(b: Button, c: Color) -> void: - for n in ["icon_normal_color", "icon_hover_color", "icon_pressed_color", "icon_focus_color", "icon_hover_pressed_color"]: - b.add_theme_color_override(n, c) - - -func _untint_btn_icon(b: Button) -> void: - for n in ["icon_normal_color", "icon_hover_color", "icon_pressed_color", "icon_focus_color", "icon_hover_pressed_color"]: - b.remove_theme_color_override(n) - - -func _set_btn_label(b: Button, label: String) -> void: - var d: Array = _btn_labels.get(b, ["", ""]) - d[0] = label - _btn_labels[b] = d - _apply_btn_mode(b) - - -## Text + icon on the left (normal mode) or centered icon only, text as tooltip (compact mode). -func _apply_btn_mode(b: Button) -> void: - var d: Array = _btn_labels.get(b, ["", ""]) - var label: String = d[0] - var tip: String = d[1] - if _details_compact: - b.text = "" - b.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER - b.tooltip_text = label if tip == "" else label + "\n" + tip - else: - b.text = label - b.icon_alignment = HORIZONTAL_ALIGNMENT_LEFT - b.tooltip_text = tip - - -func _set_fav_state(path: String) -> void: - var fav := _is_fav(path) - _set_btn_label(_btn_fav, L.t("Retirer des favoris") if fav else L.t("Ajouter aux favoris")) - _btn_fav.icon = _icon_from("Favorites") if fav else _icon_from("NonFavorite,Favorites") - if fav: - _tint_btn_icon(_btn_fav, COLOR_FAV) - else: - _untint_btn_icon(_btn_fav) - - -## Hides empty info (« - ») to keep only what is useful. -func _sync_details_meta() -> void: - if _details_type_row == null: - return - var has_type := _lbl_type.text != "" and _lbl_type.text != "-" - var has_size := _lbl_size.text != "" and _lbl_size.text != "-" - var has_date := _lbl_modified.text != "" and _lbl_modified.text != "-" - _chip_type.visible = has_type - _chip_size.visible = has_size - _chip_date.visible = has_date - _details_type_row.visible = has_type or has_size or has_date - var path_on := _details_path.text != "" and _details_path.visible - var uid_on := _lbl_uid.text != "" and _lbl_uid.text != "-" - _field_path.visible = path_on - _field_uid.visible = uid_on - _details_card_sep.visible = path_on and uid_on - _details_card.visible = path_on or uid_on - _details_sep_info.visible = _details_card.visible - - -## Switches between buttons in a column (text + icon) and a row of icons depending on the available room. -## The height needed for normal mode is recomputed every time (no remembered value): -## current content height, minus the current button area, plus the stacked buttons. -func _update_details_layout() -> void: - if _details_scroll == null or _details_dv == null: - return - var avail := _details_scroll.size.y - if avail <= 0.0 or _details_scroll.size.x < 60.0: - return # not laid out yet - var n := 0 - for b in _btn_labels.keys(): - if b.visible: - n += 1 - if n == 0: - return - var sep := float(_details_btn_box.get_theme_constant("separation")) - var box_h := _details_btn_box.get_combined_minimum_size().y - var btn_h := _btn_open.get_combined_minimum_size().y - var need := _details_dv.get_combined_minimum_size().y - box_h + n * btn_h + (n - 1) * sep + 12.0 - if not _details_compact: - if avail < need - 0.5: - _set_details_compact(true) - elif avail >= need + 2.0: - _set_details_compact(false) - - -func _set_details_compact(on: bool) -> void: - if _details_compact == on: - return - _details_compact = on - _details_btn_box.vertical = not on - for b in _btn_labels.keys(): - _apply_btn_mode(b) - - -func _build_footer() -> Control: - var footer := HBoxContainer.new() - _status = Label.new() - _status.modulate = TEXT_SECONDARY - _status.add_theme_font_size_override("font_size", 12) - _status.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _status.clip_text = true - footer.add_child(_status) - - var btn_f := _icon_btn("Folder", L.t("+Dossier")) - btn_f.tooltip_text = L.t("Nouveau dossier") - btn_f.pressed.connect(_create_folder) - footer.add_child(btn_f) - var btn_sc := Button.new() - btn_sc.text = L.t("+Scène") - _style_ghost_button(btn_sc, 6, 2) - btn_sc.pressed.connect(_create_scene) - footer.add_child(btn_sc) - var btn_st := Button.new() - btn_st.text = "+Script" - _style_ghost_button(btn_st, 6, 2) - btn_st.pressed.connect(_create_script) - footer.add_child(btn_st) - return footer - - -func _topbar_stylebox() -> StyleBoxFlat: - return _make_stylebox(COLOR_PATH_BG, COLOR_BORDER.darkened(0.25), 6, 1) - - -# Uniform dark container of the top bar (same height, outline and corners) -func _bar_panel(inner: Control, expand: bool = false, pad_h: int = 3) -> PanelContainer: - var p := PanelContainer.new() - p.custom_minimum_size = Vector2(0, BAR_H) - p.add_theme_stylebox_override("panel", _topbar_stylebox()) - if expand: - p.size_flags_horizontal = Control.SIZE_EXPAND_FILL - var pad := _pad(pad_h, 2) - p.add_child(pad) - pad.add_child(inner) - return p - - -func _icon_btn(icon_name: String, fallback: String) -> Button: - # icon_name can contain several names separated by commas: the first existing one is used - var b := Button.new() - b.focus_mode = Control.FOCUS_NONE - _style_ghost_button(b, 5, 2) - var base := EditorInterface.get_base_control() - var found := false - for n in icon_name.split(","): - if base.has_theme_icon(n, "EditorIcons"): - b.icon = base.get_theme_icon(n, "EditorIcons") - found = true - break - if not found: - b.text = fallback - return b - - -func _ghost_box(bg: Color, h: int, v: int) -> StyleBoxFlat: - var s := StyleBoxFlat.new() - s.bg_color = bg - s.set_corner_radius_all(5) - s.content_margin_left = h - s.content_margin_right = h - s.content_margin_top = v - s.content_margin_bottom = v - return s - - -func _style_ghost_button(b: Button, h: int = 6, v: int = 3, active_bg: Color = Color.TRANSPARENT) -> void: - if active_bg.a <= 0.0: - active_bg = _ov(0.18) - # Transparent at rest, "panel" on hover; active state (toggle button) more pronounced. - # flat must stay false, otherwise Godot draws no background. - b.flat = false - b.add_theme_stylebox_override("normal", _ghost_box(Color(1, 1, 1, 0.0), h, v)) - b.add_theme_stylebox_override("hover", _ghost_box(_ov(0.10), h, v)) - b.add_theme_stylebox_override("pressed", _ghost_box(active_bg, h, v)) - b.add_theme_stylebox_override("hover_pressed", _ghost_box(active_bg.lightened(0.1), h, v)) - b.add_theme_stylebox_override("disabled", _ghost_box(Color(1, 1, 1, 0.0), h, v)) - b.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) - _add_press_bounce(b) - - -## Micro-interaction shared by all "ghost" buttons: slight zoom on hover, -## small "squash" on click. Gives the drawer a livelier, more responsive feel. -func _add_press_bounce(b: Button, hover_scale: float = 1.08, press_scale: float = 0.9) -> void: - b.pivot_offset = b.size * 0.5 - b.resized.connect(func() -> void: b.pivot_offset = b.size * 0.5) - b.mouse_entered.connect(func() -> void: _bounce_to(b, Vector2.ONE * hover_scale, 0.12)) - b.mouse_exited.connect(func() -> void: _bounce_to(b, Vector2.ONE, 0.16)) - b.button_down.connect(func() -> void: _bounce_to(b, Vector2.ONE * press_scale, 0.06)) - b.button_up.connect(func() -> void: - var target := Vector2.ONE * hover_scale if b.is_hovered() else Vector2.ONE - _bounce_to(b, target, 0.14) - ) - - -func _bounce_to(node: Control, target: Vector2, dur: float) -> void: - if not is_instance_valid(node): - return - var tw := node.get_meta("_bounce_tween", null) - if tw != null and (tw as Tween).is_valid(): - (tw as Tween).kill() - var t := create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) - t.tween_property(node, "scale", target, dur) - node.set_meta("_bounce_tween", t) - - -## Softer selection/hover highlight (rounded corners + slight accent glow) -## than the default square rectangle of the ItemList. -func _style_item_list(list: ItemList) -> void: - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - # Selection: tinted background + crisp 2 px outline + slight glow (the ✓ badge is drawn on top). - var sel := _make_stylebox(Color(accent.r, accent.g, accent.b, 0.20), Color(accent.r, accent.g, accent.b, 0.95), 8, 2) - sel.shadow_color = Color(accent.r, accent.g, accent.b, 0.35) - sel.shadow_size = 5 - list.add_theme_stylebox_override("selected", sel) - list.add_theme_stylebox_override("selected_focus", sel) - list.add_theme_stylebox_override("cursor", _make_stylebox(Color.TRANSPARENT, Color(accent.r, accent.g, accent.b, 0.45), 8, 1)) - list.add_theme_stylebox_override("cursor_unfocused", StyleBoxEmpty.new()) - # "hovered" only exists from Godot 4.3; silently ignored on earlier versions. - list.add_theme_stylebox_override("hovered", _make_stylebox(_ov(0.07), _ov(0.14), 8, 1)) - - -func _update_nav_buttons() -> void: - if _btn_back == null: - return - _btn_back.disabled = _history_i <= 0 - _btn_fwd.disabled = _history_i >= _history.size() - 1 - _btn_up.disabled = _active_set.is_empty() and current_dir == "res://" - - -# ---------- Filters & tabs ---------- - -func _set_filter(idx: int) -> void: - _selected_filter = idx - _update_filter_pills() - _refresh() - - -func _update_filter_pills() -> void: - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - for i in range(_filter_buttons.size()): - var b := _filter_buttons[i] - var active := (i == _selected_filter) - b.add_theme_stylebox_override("normal", _pill_style(accent if active else _ov(0.05))) - b.add_theme_stylebox_override("hover", _pill_style(accent.lightened(0.15) if active else _ov(0.12))) - b.add_theme_stylebox_override("pressed", _pill_style(accent)) - b.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) - if active: - b.add_theme_color_override("font_color", Color.WHITE) - b.add_theme_color_override("font_hover_color", Color.WHITE) - else: - b.remove_theme_color_override("font_color") - b.remove_theme_color_override("font_hover_color") - - -func _pill_style(bg: Color) -> StyleBoxFlat: - var s := StyleBoxFlat.new() - s.bg_color = bg - s.set_corner_radius_all(9) - s.content_margin_left = 8 - s.content_margin_right = 8 - s.content_margin_top = 1 - s.content_margin_bottom = 1 - return s - - -func _switch_left_tab(idx: int) -> void: - _active_left_tab = idx - _tree.visible = (idx == 0) - _tree_tools.visible = (idx == 0) - _tab_list.visible = (idx != 0) - _btn_new_set.visible = (idx == 3) - _refresh_left_tab_content() - - -func _refresh_left_tab_content() -> void: - if _tree == null: - return - if _active_left_tab == 0: - if _tree_dirty: - _refresh_tree() - else: - _sync_tree_selection() - return - var base := EditorInterface.get_base_control() - _tab_list.clear() - if _active_left_tab == 3: - for n in _sets.keys(): - var idx := _tab_list.add_item("%s (%d)" % [n, (_sets[n] as Array).size()], base.get_theme_icon("Groups", "EditorIcons")) - _tab_list.set_item_metadata(idx, str(n)) - _tab_list.set_item_tooltip(idx, L.t("Clic droit : supprimer le set")) - return - if _active_left_tab == 1: - _sync_favorites() - var src: PackedStringArray = _favorites if _active_left_tab == 1 else _recents - var colors: Dictionary = ProjectSettings.get_setting("file_customization/folder_colors", {}) - for path in src: - var is_dir := DirAccess.dir_exists_absolute(path) - var idx := _tab_list.add_item(path.get_file() if path != "res://" else "res://", base.get_theme_icon("Folder" if is_dir else "File", "EditorIcons")) - _tab_list.set_item_metadata(idx, path) - _tab_list.set_item_tooltip(idx, path + (L.t("\nClic droit : retirer") if _active_left_tab == 1 else "")) - if is_dir: - var cn := _resolve_color(colors, path) - if cn in FOLDER_COLORS: - _tab_list.set_item_icon_modulate(idx, FOLDER_COLORS[cn]) - - -func _on_tab_list_activated(idx: int) -> void: - var meta := str(_tab_list.get_item_metadata(idx)) - if _active_left_tab == 3: - _active_set = meta - _search.text = "" - _refresh() - else: - _open_path(meta) - - -func _on_tab_list_clicked(idx: int, _at: Vector2, btn: int) -> void: - if btn != MOUSE_BUTTON_RIGHT: - return - var meta := str(_tab_list.get_item_metadata(idx)) - if _active_left_tab == 1: - _toggle_favorite(meta) - _refresh() - elif _active_left_tab == 3: - _confirm(L.t("Supprimer le set"), L.t("Supprimer le set « %s » ? (les fichiers ne sont pas touchés)") % meta, func() -> void: - _sets.erase(meta) - if _active_set == meta: - _active_set = "" - _save_cfg() - _refresh() - ) - - - -# ---------- Anti-blur HD rendering ---------- - -func _sd_rounded_box(p: Vector2, b: Vector2, r: float) -> float: - var q := Vector2(absf(p.x), absf(p.y)) - b + Vector2(r, r) - return minf(maxf(q.x, q.y), 0.0) + Vector2(maxf(q.x, 0.0), maxf(q.y, 0.0)).length() - r - - -func _bucket(size: int) -> int: - for b in ICON_BUCKETS: - if size <= int(b): - return int(b) - return int(ICON_BUCKETS[ICON_BUCKETS.size() - 1]) - - -func _get_hd_folder_icon(size: int) -> Texture2D: - # Grayscale icon: the folder color is applied by modulation - # (a single texture per size instead of one per color). - var cache_key := "folder_%d" % size - if _hd_icon_cache.has(cache_key): - return _hd_icon_cache[cache_key] - - var img := Image.create(size, size, false, Image.FORMAT_RGBA8) - var S := float(size) - var tab_center := Vector2(S * 0.32, S * 0.26) - var tab_size := Vector2(S * 0.22, S * 0.09) - var tab_radius := S * 0.05 - var body_center := Vector2(S * 0.50, S * 0.58) - var body_size := Vector2(S * 0.42, S * 0.28) - var body_radius := S * 0.07 - - var tab_color := Color(0.78, 0.78, 0.78) # = darkened(0.22) once tinted - var body_color := Color.WHITE - - for y in range(size): - for x in range(size): - var p := Vector2(x + 0.5, y + 0.5) - var d_tab := _sd_rounded_box(p - tab_center, tab_size, tab_radius) - var d_body := _sd_rounded_box(p - body_center, body_size, body_radius) - var a_tab := clampf(0.5 - d_tab, 0.0, 1.0) - var a_body := clampf(0.5 - d_body, 0.0, 1.0) - var c := Color(0, 0, 0, 0) - if a_tab > 0.0: - c = Color(tab_color.r, tab_color.g, tab_color.b, a_tab) - if a_body > 0.0: - c = c.blend(Color(body_color.r, body_color.g, body_color.b, a_body)) - img.set_pixel(x, y, c) - - var tex := ImageTexture.create_from_image(img) - _hd_icon_cache[cache_key] = tex - return tex - - -func _get_card_image(size: int) -> Image: - # Common "card" background for all file types, computed only once per size - if _card_images.has(size): - return _card_images[size] - var img := Image.create(size, size, false, Image.FORMAT_RGBA8) - var S := float(size) - var card_center := Vector2(S * 0.50, S * 0.52) - var card_size := Vector2(S * 0.36, S * 0.40) - var card_radius := S * 0.08 - var card_color := Color(0.16, 0.20, 0.26, 0.95) - var border_color := Color(0.35, 0.45, 0.60, 0.8) - for y in range(size): - for x in range(size): - var p := Vector2(x + 0.5, y + 0.5) - var d := _sd_rounded_box(p - card_center, card_size, card_radius) - var a := clampf(0.5 - d, 0.0, 1.0) - if a > 0.0: - var col := border_color if (d > -1.5) else card_color - img.set_pixel(x, y, Color(col.r, col.g, col.b, col.a * a)) - _card_images[size] = img - return img - - -func _raw_file_icon(base: Control, type: String) -> Texture2D: - var icon_name := type if base.has_theme_icon(type, "EditorIcons") else "File" - return base.get_theme_icon(icon_name, "EditorIcons") - - -func _get_hd_file_icon(base: Control, type: String, size: int, with_card: bool = true) -> Texture2D: - var icon_name := type if base.has_theme_icon(type, "EditorIcons") else "File" - if base.has_theme_icon(icon_name + "Big", "EditorIcons"): - icon_name += "Big" - var cache_key := "file_%s_%d_%s" % [icon_name, size, "card" if with_card else "flat"] - if _hd_icon_cache.has(cache_key): - return _hd_icon_cache[cache_key] - - var raw_tex := base.get_theme_icon(icon_name, "EditorIcons") - var raw_img := raw_tex.get_image() - if raw_img == null: - return raw_tex - if raw_img.is_compressed(): - raw_img.decompress() - raw_img.convert(Image.FORMAT_RGBA8) - - var img: Image - var inner_size: int - if with_card: - # Decorative "card" thumbnail (grid): icon at 52 % for a consistent look across types. - img = _get_card_image(size).duplicate() as Image - inner_size = int(size * 0.52) - else: - # No card: the container that displays this texture already has its own frame - # (details panel), so the icon fills the thumbnail instead of floating in the middle - # of a large transparent area. - img = Image.create(size, size, false, Image.FORMAT_RGBA8) - inner_size = int(size * 0.82) - raw_img.resize(inner_size, inner_size, Image.INTERPOLATE_LANCZOS) - var offset := int((size - inner_size) / 2.0) - img.blend_rect(raw_img, Rect2i(0, 0, inner_size, inner_size), Vector2i(offset, offset)) - - var tex := ImageTexture.create_from_image(img) - _hd_icon_cache[cache_key] = tex - return tex - - - - -# ---------- Folder colors (inherited, like in the FileSystem dock) ---------- - -func _colors() -> Dictionary: - return ProjectSettings.get_setting("file_customization/folder_colors", {}) - - -func _resolve_color(colors: Dictionary, path: String) -> String: - var p := path.trim_suffix("/") - var guard := 0 - while guard < 64: - guard += 1 - var v := str(colors.get(p + "/", colors.get(p, ""))) - if v != "": - return v - if p == "res:" or p == "res:/" or p == "" or p == "res://": - break - var parent := p.get_base_dir() - if parent == p: - break - p = parent - return "" - - -func _folder_color(colors: Dictionary, path: String) -> Color: - return FOLDER_COLORS.get(_resolve_color(colors, path), DEFAULT_FOLDER_COLOR) - - -func _set_folder_color(path: String, color_key: String) -> void: - var colors: Dictionary = _colors().duplicate() - var p := path.trim_suffix("/") - colors.erase(p) - colors.erase(p + "/") - if not color_key.is_empty(): - colors[p + "/"] = color_key - ProjectSettings.set_setting("file_customization/folder_colors", colors) - ProjectSettings.save() - _tree_dirty = true - _refresh() - - -# ---------- Refresh ---------- - -func _refresh() -> void: - if not is_inside_tree() or _list == null: - return - _sync_favorites() - _dirty = false - var base := EditorInterface.get_base_control() - var prev_selected := _selected_paths() - var isz := int(_current_zoom()) - _icon_bucket = _bucket(isz) - _apply_list_layout() - - _list.clear() - _path_index.clear() - _item_colors.clear() - _dir_paths.clear() - # If the current folder is gone, go up - var fs := EditorInterface.get_resource_filesystem() - while _active_set.is_empty() and current_dir != "res://" and (fs.get_filesystem_path(current_dir) == null or (not _show_hidden and _is_hidden(current_dir))): - current_dir = current_dir.trim_suffix("/").get_base_dir() - if current_dir == "res:": - current_dir = "res://" - _refresh_breadcrumbs() - _update_nav_buttons() - _refresh_left_tab_content() - - # entry = [path, type, is_folder, name] - var query := _search.text.strip_edges().to_lower() - var tokens := query.split(" ", false) - var entries: Array = [] - if not _active_set.is_empty(): - for p in (_sets.get(_active_set, []) as Array): - var ps := str(p) - if not tokens.is_empty() and not _name_matches(ps.trim_suffix("/").get_file().to_lower(), tokens): - continue - if DirAccess.dir_exists_absolute(ps): - entries.append([ps, "Folder", true, ps.trim_suffix("/").get_file()]) - elif FileAccess.file_exists(ps) and _pass_filter(ps, _selected_filter): - entries.append([ps, fs.get_file_type(ps), false, ps.get_file()]) - elif query.is_empty(): - var dir := fs.get_filesystem_path(current_dir) - if dir != null: - for i in dir.get_subdir_count(): - var sub := dir.get_subdir(i) - if not _show_hidden and _is_hidden(sub.get_path()): - continue - entries.append([sub.get_path(), "Folder", true, sub.get_name()]) - for i in dir.get_file_count(): - var fp := dir.get_file_path(i) - if _pass_filter(fp, _selected_filter) and (_show_hidden or not _is_hidden(fp)): - entries.append([fp, dir.get_file_type(i), false, dir.get_file(i)]) - else: - _search_entries(tokens, entries) - - entries.sort_custom(func(a: Array, b: Array) -> bool: - # entry = [path, type, is_folder, name, rank] (rank: 0 exact name, 1 starts with, 2 contains) - var ra: int = a[4] if a.size() > 4 else 0 - var rb: int = b[4] if b.size() > 4 else 0 - if ra != rb: - return ra < rb - if a[2] != b[2]: - return a[2] - return str(a[3]).naturalnocasecmp_to(str(b[3])) < 0 - ) - _truncated = entries.size() > MAX_ITEMS - if _truncated: - entries.resize(MAX_ITEMS) - - var custom_colors := _colors() - var folder_tex := _get_hd_folder_icon(_icon_bucket) - var show_parent := not query.is_empty() and _active_set.is_empty() - var check_hidden := _show_hidden and not _hidden.is_empty() - for e in entries: - var path: String = e[0] - var type: String = e[1] - var is_dir: bool = e[2] - var label: String = e[3] - var is_hid := check_hidden and _is_hidden(path) - if show_parent: - # Results coming from several folders: show where they come from - var parent := path.trim_suffix("/").get_base_dir().trim_prefix("res://") - if parent.is_empty(): - parent = "res://" - label += (" · " + parent) if _view_list else ("\n" + parent) - var idx: int - if is_dir: - idx = _list.add_item(label, folder_tex) - var fc := _folder_color(custom_colors, path) - if is_hid: - fc.a *= HIDDEN_ALPHA - _list.set_item_icon_modulate(idx, fc) - _dir_paths[path] = true - else: - var tex: Texture2D = _preview_cache.get(path, null) - if tex == null: - tex = _raw_file_icon(base, type) if (_view_list and _icon_bucket <= 24) else _get_hd_file_icon(base, type, _icon_bucket) - idx = _list.add_item(label, tex) - if is_hid: - _list.set_item_icon_modulate(idx, Color(1, 1, 1, HIDDEN_ALPHA)) - _list.set_item_metadata(idx, path) - _list.set_item_tooltip(idx, path + (("\n" + L.t("Masqué dans le tiroir")) if is_hid else "")) - _path_index[path] = idx - _item_colors.append(Color(0, 0, 0, 0) if is_dir else _type_color(path, type)) - if _is_fav(path): - _list.set_item_custom_fg_color(idx, COLOR_FAV) - if is_hid: - _list.set_item_custom_fg_color(idx, Color(1, 1, 1, HIDDEN_ALPHA)) - var shown := _list.item_count - - # Restore the selection (or select the newly created item) - var reselect := prev_selected - if _pending_select != "": - reselect = PackedStringArray([_pending_select]) - for i in _list.item_count: - var mp := str(_list.get_item_metadata(i)) - if reselect.has(mp) or reselect.has(mp.trim_suffix("/")): - _list.select(i, false) - if _pending_select != "": - _list.ensure_current_is_visible() - _pending_select = "" - - _empty_state.visible = (shown == 0) - _empty_reset.visible = false - if shown == 0: - if _selected_filter != 0: - _empty_lbl.text = L.t("Aucun résultat avec ce filtre") - _empty_link.visible = false - _empty_reset.visible = true - elif not _active_set.is_empty(): - _empty_lbl.text = L.t("Set vide") - _empty_link.visible = false - elif not query.is_empty(): - _empty_lbl.text = L.t("Aucun résultat") - _empty_link.visible = false - else: - _empty_lbl.text = L.t("Aucun asset") - _empty_link.visible = true - _on_selection_changed(false) - _schedule_previews() - - -## Strip color of an asset according to its type (resource class, otherwise extension). -func _type_color(path: String, type: String) -> Color: - var ext := path.get_extension().to_lower() - var known := type != "" and ClassDB.class_exists(type) - var kind := "resource" - if ext in ["obj", "fbx", "glb", "gltf", "blend", "dae"] or (known and ClassDB.is_parent_class(type, "Mesh")): - kind = "model" - elif ext in ["tscn", "scn"] or type == "PackedScene": - kind = "scene" - elif ext in ["gd", "cs", "cpp", "h"] or (known and ClassDB.is_parent_class(type, "Script")): - kind = "script" - elif ext in ["png", "jpg", "jpeg", "svg", "webp", "bmp", "tga", "exr", "hdr", "dds", "ktx"] or (known and ClassDB.is_parent_class(type, "Texture")): - kind = "image" - elif ext in ["wav", "ogg", "mp3"] or (known and ClassDB.is_parent_class(type, "AudioStream")): - kind = "audio" - elif ext in ["gdshader", "shader", "gdshaderinc"] or (known and ClassDB.is_parent_class(type, "Shader")): - kind = "shader" - elif known and ClassDB.is_parent_class(type, "Material"): - kind = "material" - elif type.begins_with("Animation") or (known and ClassDB.is_parent_class(type, "Animation")): - kind = "animation" - elif ext in ["ttf", "otf", "woff", "woff2", "fnt"] or (known and ClassDB.is_parent_class(type, "Font")): - kind = "font" - return TYPE_COLORS[kind] - - -# ItemList.get_item_rect() does not account for scrolling in every Godot version -# (rectangle in content coordinates instead of screen coordinates): the type strips, the -# badges and the "Copy" button then stayed fixed while the list was scrolled. -# We detect it once with a hover test (which is always correct), then correct for it. -var _rect_includes_scroll := -1 # -1 unknown, 0: rect without scroll, 1: rect already on screen - - -func _item_rect(i: int, expand: bool = true) -> Rect2: - var r := _list.get_item_rect(i, expand) - var sv := _list.get_v_scroll_bar().value - if sv <= 0.0: - return r - if _rect_includes_scroll < 0: - _detect_rect_scroll(sv) - if _rect_includes_scroll == 0: - r.position.y -= sv - return r - - -func _detect_rect_scroll(sv: float) -> void: - var xs: Array[float] = [24.0, maxf(float(_list.fixed_column_width) * 0.5, 24.0), 80.0] - var y := 6.0 - while y < _list.size.y: - for x in xs: - var p := Vector2(x, y) - var idx := _list.get_item_at_position(p, true) - if idx < 0: - continue - var r := _list.get_item_rect(idx, true) - var as_screen := r.has_point(p) - r.position.y -= sv - var as_content := r.has_point(p) - if as_screen != as_content: # unambiguous result - _rect_includes_scroll = 1 if as_screen else 0 - return - y += 10.0 - - -## Draws, on top of the list, the colored indicator of each visible item: -## grid = bar inset at the bottom of the thumbnail; list = vertical bar on the left. -func _draw_type_bars() -> void: - if not SHOW_TYPE_BAR or _list == null: - return - var n := mini(_list.item_count, _item_colors.size()) - if n == 0: - return - if _bar_style == null: - _bar_style = StyleBoxFlat.new() - _bar_style.set_corner_radius_all(2) - # Thin dark outline: the bar stays readable even on a thumbnail of a color - # close to its own (without it, a blue thumbnail with a blue "scene" bar - # would almost completely blend into it). - _bar_style.border_color = Color(0, 0, 0, 0.55) - _bar_style.set_border_width_all(1) - var view_h := _list.size.y - var icon_w := float(_list.fixed_icon_size.x) - var margin := float(_list.get_theme_constant("icon_margin")) - var thick := clampf(margin - 1.0, 3.0, 5.0) - for i in n: - var c := _item_colors[i] - if c.a <= 0.0: - continue - var r := _item_rect(i, true) - if r.end.y < 0.0: - continue - if r.position.y > view_h: - break # items are laid out top to bottom: the rest is off screen - _bar_style.bg_color = Color(c.r, c.g, c.b, 0.95) - if _view_list: - _list.draw_style_box(_bar_style, Rect2(r.position.x + 1.0, r.position.y + 3.0, 3.0, maxf(r.size.y - 6.0, 4.0))) - else: - var x := r.position.x + floorf((r.size.x - icon_w) * 0.5) - # Inset in the last pixels of the thumbnail (not in the margin before the - # text): so it can never overlap the file name, even on 2 lines. - var y := r.position.y + margin + icon_w - thick - 1.0 - _list.draw_style_box(_bar_style, Rect2(x, y, icon_w, thick)) - - -## On top of the list: ✓ badges of selected items, drop target (highlighted folder with -## "Move here" pill, or frame around the list) and the selection rectangle. -func _draw_list_overlay() -> void: - if _list == null: - return - var acc: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - var view_h := _list.size.y - - if not _view_list: - for i in _list.get_selected_items(): - var r := _item_rect(i, true) - if r.end.y < 0.0: - continue - if r.position.y > view_h: - break - var c := Vector2(r.end.x - 11.0, r.position.y + 11.0) - _list.draw_circle(c, 7.5, acc) - _list.draw_polyline(PackedVector2Array([c + Vector2(-3.2, 0.3), c + Vector2(-0.8, 2.7), c + Vector2(3.4, -2.3)]), Color.WHITE if acc.get_luminance() < 0.6 else Color.BLACK, 1.6, true) - - if _drop_whole: - _list.draw_style_box(_make_stylebox(Color(acc.r, acc.g, acc.b, 0.06), Color(acc.r, acc.g, acc.b, 0.75), 8, 2), Rect2(Vector2.ZERO, _list.size)) - - if _drop_list_idx >= 0 and _drop_list_idx < _list.item_count: - var r2 := _item_rect(_drop_list_idx, true) - var glow := _make_stylebox(Color(acc.r, acc.g, acc.b, 0.26), acc, 9, 2) - glow.shadow_color = Color(acc.r, acc.g, acc.b, 0.5) - glow.shadow_size = 8 - _list.draw_style_box(glow, r2) - var font := _list.get_theme_font("font") - var fs := 10 - var txt := L.t("Déplacer ici") - var tw := font.get_string_size(txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x - var pr := Rect2(r2.position.x + (r2.size.x - (tw + 14.0)) * 0.5, r2.position.y + 3.0, tw + 14.0, 16.0) - _list.draw_style_box(_make_stylebox(acc, Color.TRANSPARENT, 8), pr) - _list.draw_string(font, Vector2(pr.position.x + 7.0, pr.position.y + 11.5), txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, Color.WHITE if acc.get_luminance() < 0.6 else Color.BLACK) - - if _mq_active and _mq_moved: - var sv := Vector2(_mq_start.x, _mq_start.y - _list.get_v_scroll_bar().value) - var mr := Rect2(sv, _mq_cur - sv).abs() - _list.draw_style_box(_make_stylebox(Color(acc.r, acc.g, acc.b, 0.16), Color(acc.r, acc.g, acc.b, 0.9), 3, 1), mr) - - -## Tree folder targeted during a drag and drop: rounded accent frame. -func _draw_tree_drop() -> void: - if _drop_tree_item == null or not is_instance_valid(_drop_tree_item): - return - var acc: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - var r := _tree.get_item_area_rect(_drop_tree_item, 0).grow(-1.0) - var st := _make_stylebox(Color(acc.r, acc.g, acc.b, 0.25), acc, 6, 2) - st.shadow_color = Color(acc.r, acc.g, acc.b, 0.4) - st.shadow_size = 5 - _tree.draw_style_box(st, r) - - -func _current_zoom() -> float: - return _zoom_list if _view_list else _zoom_grid - - -# Adapts the slider range to the current mode and restores the remembered zoom of THIS mode -func _configure_zoom() -> void: - if _zoom == null: - return - _zoom.set_block_signals(true) - if _view_list: - _zoom.min_value = 16 - _zoom.max_value = 48 - _zoom.step = 2 - _zoom.value = clampf(_zoom_list, 16.0, 48.0) - _zoom.tooltip_text = L.t("Taille des icônes en liste (Ctrl+molette)") - else: - _zoom.min_value = 48 - _zoom.max_value = 160 - _zoom.step = 8 - _zoom.value = clampf(_zoom_grid, 48.0, 160.0) - _zoom.tooltip_text = L.t("Taille des miniatures en grille (Ctrl+molette)") - _zoom.set_block_signals(false) - - -func _apply_list_layout() -> void: - var s := int(_current_zoom()) - if _view_list: - _list.icon_mode = ItemList.ICON_MODE_LEFT - _list.max_columns = 1 - _list.max_text_lines = 1 - _list.fixed_icon_size = Vector2i(s, s) - _list.fixed_column_width = 0 - else: - _list.icon_mode = ItemList.ICON_MODE_TOP - _list.max_columns = 0 - _list.max_text_lines = 2 - _list.fixed_icon_size = Vector2i(s, s) - _list.fixed_column_width = s + 28 - - -## Called when the user releases the separator between the list and the details panel. -## `offset` is already clamped by the SplitContainer (respects the minimums of both sides). -func _on_details_split_dragged(offset: int) -> void: - _details_width = maxf(DETAILS_MIN_WIDTH - float(offset), DETAILS_MIN_WIDTH) - _save_cfg() - - -func _on_zoom_changed(v: float) -> void: - if _view_list: - _zoom_list = v - else: - _zoom_grid = v - _save_cfg() - if not is_open: - _dirty = true - return - # Zoom only affects the layout; the list is rebuilt only if the - # generated icon size changes tier. - _apply_list_layout() - if _bucket(int(v)) != _icon_bucket: - _refresh() - else: - _schedule_previews() - - -# Zoom slider readable on a dark background: light track, filled part in accent, round handle -func _style_zoom_slider(accent: Color) -> void: - var track := StyleBoxFlat.new() - track.bg_color = COLOR_TRACK - track.set_corner_radius_all(2) - track.content_margin_top = 2 - track.content_margin_bottom = 2 - var fill := track.duplicate() as StyleBoxFlat - fill.bg_color = accent - var fill_hi := track.duplicate() as StyleBoxFlat - fill_hi.bg_color = accent.lightened(0.2) - _zoom.add_theme_stylebox_override("slider", track) - _zoom.add_theme_stylebox_override("grabber_area", fill) - _zoom.add_theme_stylebox_override("grabber_area_highlight", fill_hi) - _zoom.add_theme_icon_override("grabber", _make_dot(12, TEXT_PRIMARY)) - _zoom.add_theme_icon_override("grabber_highlight", _make_dot(12, Color.WHITE)) - _zoom.add_theme_icon_override("grabber_disabled", _make_dot(12, COLOR_TRACK)) - - -func _make_dot(d: int, col: Color) -> ImageTexture: - var img := Image.create(d, d, false, Image.FORMAT_RGBA8) - var c := (d - 1) * 0.5 - var r := d * 0.5 - 0.5 - for y in d: - for x in d: - var dist := Vector2(x - c, y - c).length() - img.set_pixel(x, y, Color(col.r, col.g, col.b, clampf(r + 0.5 - dist, 0.0, 1.0))) - return ImageTexture.create_from_image(img) - - -# ---------- Lazy thumbnails ---------- - -func _schedule_previews() -> void: - if _preview_timer != null and is_open: - _preview_timer.start() - - -func _request_visible_previews() -> void: - # Only requests thumbnails of visible items (+ one page of margin on each side) - if not is_open or _list == null or _list.item_count == 0: - return - if _list.has_method("force_update_list_size"): - _list.call("force_update_list_size") - var first := _list.get_item_at_position(Vector2(6, 6), false) - var last := _list.get_item_at_position(Vector2(_list.size.x - 6, _list.size.y - 6), false) - if first < 0 or last < first: - first = 0 - last = mini(_list.item_count - 1, 59) - var span := maxi(last - first + 1, 24) - var lo := maxi(first - span, 0) - var hi := mini(last + span, _list.item_count - 1) - var previewer := EditorInterface.get_resource_previewer() - for i in range(lo, hi + 1): - var path := str(_list.get_item_metadata(i)) - if _dir_paths.has(path) or _preview_pending.has(path): - continue - if path.get_extension().to_lower() == "svg" and not _svg_fail.has(path): - # Godot's thumbnail is small (≈ 64 px) then enlarged: blurry. We rasterize the - # SVG at display size, in small batches so the interface doesn't stutter. - var target := _svg_target() - if _preview_cache.has(path) and int(_svg_px.get(path, 0)) >= target: - continue - _preview_pending[path] = true - _svg_queue.append([path, target]) - continue - if _preview_cache.has(path): - continue - _preview_pending[path] = true - previewer.queue_resource_preview(path, self, "_on_preview", path) - - -## All the search words must appear in the name (already lowercase). -func _name_matches(name_lc: String, tokens: PackedStringArray) -> bool: - for t in tokens: - if not name_lc.contains(t): - return false - return true - - -## Flat index (path, type, folder?, name, lowercase name) of the whole project. -## Rebuilt only when the file system changes: a search no longer walks -## the editor's tree, just this array. -func _build_index() -> void: - _index.clear() - var fs := EditorInterface.get_resource_filesystem().get_filesystem() - if fs != null: - _index_dir(fs) - _index_dirty = false - - -func _index_dir(dir: EditorFileSystemDirectory) -> void: - for i in dir.get_subdir_count(): - var sub := dir.get_subdir(i) - var sname := sub.get_name() - _index.append([sub.get_path(), "Folder", true, sname, sname.to_lower()]) - _index_dir(sub) - for i in dir.get_file_count(): - var fname := dir.get_file(i) - _index.append([dir.get_file_path(i), dir.get_file_type(i), false, fname, fname.to_lower()]) - - -## Search across the whole project (or under the current folder). Results ranked: -## exact name, then name starting with the 1st word, then name containing it. -func _search_entries(tokens: PackedStringArray, out: Array) -> void: - if tokens.is_empty(): - return - if _index_dirty: - _build_index() - var prefix := "" - if not _search_all: - prefix = current_dir if current_dir.ends_with("/") else current_dir + "/" - var check_hidden := not _show_hidden and not _hidden.is_empty() - var cap := MAX_ITEMS * 4 - var first: String = tokens[0] - for e in _index: - var p: String = e[0] - if prefix != "" and (p == prefix or not p.begins_with(prefix)): - continue - var nm: String = e[4] - if not _name_matches(nm, tokens): - continue - if not e[2] and not _pass_filter(p, _selected_filter): - continue - if check_hidden and _is_hidden(p): - continue - var rank := 2 - if nm.get_basename() == first or nm == first: - rank = 0 - elif nm.begins_with(first): - rank = 1 - out.append([p, e[1], e[2], e[3], rank]) - if out.size() > cap: - break - - -func _on_scope_toggled(on: bool) -> void: - _search_all = on - _update_search_ui() - _save_cfg() - if not _search.text.strip_edges().is_empty(): - _refresh() - - -func _update_search_ui() -> void: - if _search != null: - _search.placeholder_text = L.t("Rechercher dans tout le projet... (Ctrl+F)") if _search_all else L.t("Rechercher dans ce dossier... (Ctrl+F)") - if _btn_scope != null: - _btn_scope.tooltip_text = L.t("Recherche : tout le projet (cliquer pour limiter au dossier courant)") if _search_all else L.t("Recherche : dossier courant (cliquer pour chercher dans tout le projet)") - - -# ---------- Hidden items ---------- - -## Stored key: a folder ends with "/", a file keeps its exact path. -func _hidden_key(path: String) -> String: - if DirAccess.dir_exists_absolute(path): - return path.trim_suffix("/") + "/" - return path - - -func _is_hidden(path: String) -> bool: - if _hidden.is_empty(): - return false - var pd := path if path.ends_with("/") else path + "/" - for h in _hidden: - if h.ends_with("/"): - if pd.begins_with(h): - return true - elif path == h: - return true - return false - - -func _hide_paths(paths: PackedStringArray, hide: bool) -> void: - for p in paths: - if p == "res://": - continue - var key := _hidden_key(p) - if hide: - if not _hidden.has(key): - _hidden.append(key) - else: - var i := _hidden.find(key) - if i >= 0: - _hidden.remove_at(i) - _tree_dirty = true - _save_cfg() - _list.deselect_all() - _refresh() - if hide and not _show_hidden: - _flash(L.t("Masqué dans le tiroir · clic droit → « Afficher les éléments masqués »")) - else: - _flash(L.t("Masqué dans le tiroir") if hide else L.t("Réaffiché")) - - -func _set_show_hidden(on: bool) -> void: - _show_hidden = on - _tree_dirty = true - _save_cfg() - _refresh() - - -func _pass_filter(path: String, filter_idx: int) -> bool: - if filter_idx <= 0: - return true - var ext := path.get_extension().to_lower() - match filter_idx: - 1: return ext in ["tscn", "scn"] - 2: return ext in ["gd", "cs", "cpp", "h"] - 3: return ext in ["obj", "fbx", "glb", "gltf", "blend"] - 4: return ext in ["png", "jpg", "jpeg", "svg", "webp", "bmp", "tga", "exr", "hdr"] - 5: return ext in ["wav", "ogg", "mp3"] - 6: return ext in ["gdshader", "shader", "gdshaderinc"] - return true - - -func _selected_paths() -> PackedStringArray: - var out: PackedStringArray = [] - if _list == null: - return out - for i in _list.get_selected_items(): - out.append(str(_list.get_item_metadata(i))) - return out - - -func _on_selection_changed(select_in_dock: bool = true) -> void: - var sel := _selected_paths() - if sel.size() == 0: - _update_details(current_dir if _active_set.is_empty() else "res://") - elif sel.size() == 1: - if select_in_dock: - EditorInterface.select_file(sel[0]) - _update_details(sel[0]) - else: - _context_path = sel[0] - _details_name.text = L.t("%d éléments sélectionnés") % sel.size() - _details_path.text = "" - _lbl_type.text = "-" - _lbl_size.text = "-" - _lbl_modified.text = "-" - _lbl_uid.text = "-" - _set_preview_tex(null) - _preview.self_modulate = Color.WHITE - _details_path.visible = false - _sync_details_meta() - _update_details_layout.call_deferred() - _update_status_bar() - - -func _update_status_bar(_unused: int = -1) -> void: - var total := _list.item_count - var sel_count := _list.get_selected_items().size() - var where := current_dir if _active_set.is_empty() else L.t("Set : ") + _active_set - var txt := L.t("%d élément%s") % [total, "s" if total > 1 else ""] - if _truncated: - txt += L.t(" (limité à %d)") % MAX_ITEMS - if sel_count > 0: - txt += L.t(" · %d sélectionné%s") % [sel_count, L.sel_s(sel_count > 1)] - _status.text = txt + " · " + where - - -func _flash(msg: String, is_error: bool = false) -> void: - _status.text = msg - get_tree().create_timer(2.5).timeout.connect(_update_status_bar) - _show_toast(msg, is_error) - - -func _show_toast(msg: String, is_error: bool) -> void: - if _toast == null or not is_instance_valid(_toast): - return - _toast_lbl.text = msg - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - var col: Color = COLOR_DANGER if is_error else accent - _toast.add_theme_stylebox_override("panel", _make_stylebox(COLOR_POPUP_BG, Color(col.r, col.g, col.b, 0.85), 8, 1)) - if _toast_tween and _toast_tween.is_valid(): - _toast_tween.kill() - _toast.pivot_offset = _toast.size * 0.5 - _toast.scale = Vector2(0.88, 0.88) - _toast.modulate.a = 0.0 - _toast_tween = create_tween() - _toast_tween.tween_property(_toast, "modulate:a", 1.0, 0.16).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - _toast_tween.parallel().tween_property(_toast, "scale", Vector2.ONE, 0.22).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) - _toast_tween.tween_interval(1.7) - _toast_tween.tween_property(_toast, "modulate:a", 0.0, 0.3).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) - _toast_tween.parallel().tween_property(_toast, "scale", Vector2(0.94, 0.94), 0.3).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) - - -func _rel_time(mtime: int) -> String: - var diff := int(Time.get_unix_time_from_system()) - mtime - if diff < 60: - return L.t("à l'instant") - if diff < 3600: - return L.t("il y a %d min") % (diff / 60) - if diff < 86400: - return L.t("il y a %d h") % (diff / 3600) - if diff < 172800: - return L.t("hier") - var d := Time.get_date_dict_from_unix_time(mtime) - if L.is_fr(): - return "%02d/%02d/%d" % [d.day, d.month, d.year] - return "%d-%02d-%02d" % [d.year, d.month, d.day] - - -func _update_details(path: String) -> void: - _context_path = path - var is_dir := DirAccess.dir_exists_absolute(path) - _details_name.text = _soft_wrap("res://" if path == "res://" else path.trim_suffix("/").get_file()) - _details_path.text = _soft_wrap(path) - _details_path.visible = path != "res://" - if is_dir: - _lbl_type.text = L.t("Dossier") - var n := DirAccess.get_directories_at(path).size() + DirAccess.get_files_at(path).size() - _lbl_size.text = L.t("%d élément%s") % [n, "s" if n > 1 else ""] - _lbl_modified.text = "-" - _lbl_uid.text = "-" - _set_preview_tex(_get_hd_folder_icon(96)) - _preview.self_modulate = _folder_color(_colors(), path) - else: - var t := EditorInterface.get_resource_filesystem().get_file_type(path) - _preview.self_modulate = Color.WHITE - _lbl_type.text = t if t != "" else path.get_extension().to_upper() - _lbl_size.text = "-" - _lbl_modified.text = "-" - _lbl_uid.text = "-" - if FileAccess.file_exists(path): - var f := FileAccess.open(path, FileAccess.READ) - if f: - var bytes := f.get_length() - f.close() - if bytes < 1024: - _lbl_size.text = L.t("%d o") % bytes - elif bytes < 1024 * 1024: - _lbl_size.text = L.t("%.1f Ko") % (bytes / 1024.0) - else: - _lbl_size.text = L.t("%.1f Mo") % (bytes / (1024.0 * 1024.0)) - var mtime := FileAccess.get_modified_time(path) - if mtime > 0: - _lbl_modified.text = _rel_time(mtime) - if ResourceLoader.has_method("get_resource_uid"): - var uid: int = ResourceLoader.call("get_resource_uid", path) - if uid != -1: - _lbl_uid.text = ResourceUID.id_to_text(uid) - var cached = _preview_cache.get(path, null) - if cached != null: - _set_preview_tex(cached) - else: - _set_preview_tex(_get_hd_file_icon(EditorInterface.get_base_control(), t, 96, false)) - if not _preview_pending.has(path): - _preview_pending[path] = true - EditorInterface.get_resource_previewer().queue_resource_preview(path, self, "_on_preview", path) - _set_fav_state(path) - if is_instance_valid(_btn_copy_res): - _btn_copy_res.visible = not is_dir - _sync_details_meta() - _update_details_layout.call_deferred() - - -func _refresh_breadcrumbs() -> void: - for c in _breadcrumbs.get_children(): - _breadcrumbs.remove_child(c) - c.queue_free() - var bold := EditorInterface.get_base_control().get_theme_font("bold", "EditorFonts") - var parts: Array = [] # [text, path or ""] - if not _active_set.is_empty(): - parts.append(["Sets", ""]) - parts.append([_active_set, ""]) - else: - var segs := current_dir.split("/", false) - var build := "" - for i in range(segs.size()): - if i == 0 and segs[i] == "res:": - build = "res://" - parts.append(["res://", build]) - else: - build += segs[i] + "/" - parts.append([segs[i], build]) - for i in range(parts.size()): - var btn := Button.new() - btn.text = parts[i][0] - _style_ghost_button(btn, 6, 2) - btn.add_theme_font_size_override("font_size", 12) - btn.focus_mode = Control.FOCUS_NONE - btn.add_theme_font_override("font", bold) - var target: String = parts[i][1] - if target != "": - btn.pressed.connect(func() -> void: _navigate(target)) - else: - btn.mouse_filter = Control.MOUSE_FILTER_IGNORE # non-clickable segment: no hover - _breadcrumbs.add_child(btn) - if i < parts.size() - 1: - var sep := Label.new() - sep.text = "›" - sep.modulate = TEXT_MUTED - _breadcrumbs.add_child(sep) - - -func _refresh_tree() -> void: - _tree_dirty = false - _syncing_tree = true - _tree.clear() - _tree_items.clear() - _sticky_sig = "" - _row_pitch = 0.0 - _pending_reveal = null - var fs := EditorInterface.get_resource_filesystem().get_filesystem() - if fs == null: - _syncing_tree = false - return - var icon := EditorInterface.get_base_control().get_theme_icon("Folder", "EditorIcons") - var root := _tree.create_item() - root.set_text(0, "res://") - root.set_metadata(0, "res://") - root.set_icon(0, icon) - _tree_items["res:/"] = root - _fill_tree(root, fs, _colors(), icon) - _syncing_tree = false - _sync_tree_selection() - - -func _sync_tree_selection() -> void: - # Selects the current folder without rebuilding the tree - if _tree == null or not _active_set.is_empty(): - return - var target: TreeItem = _tree_items.get(current_dir.trim_suffix("/"), null) - if target == null: - return - _syncing_tree = true - var p := target.get_parent() - while p != null: - p.collapsed = false - p = p.get_parent() - target.select(0) - _syncing_tree = false - _reveal_tree_item.call_deferred(target) - - -func _fill_tree(parent: TreeItem, dir: EditorFileSystemDirectory, colors: Dictionary, icon: Texture2D) -> void: - for i in dir.get_subdir_count(): - var sub := dir.get_subdir(i) - var is_hid := _is_hidden(sub.get_path()) - if is_hid and not _show_hidden: - continue - var item := _tree.create_item(parent) - item.set_text(0, sub.get_name()) - item.set_metadata(0, sub.get_path()) - item.set_icon(0, icon) - var tint := _folder_color(colors, sub.get_path()) - if is_hid: - tint.a *= HIDDEN_ALPHA - item.set_custom_color(0, Color(1, 1, 1, HIDDEN_ALPHA)) - item.set_icon_modulate(0, tint) - _tree_items[sub.get_path().trim_suffix("/")] = item - _fill_tree(item, sub, colors, icon) - # Collapsed by default (large projects); the current folder is expanded on selection - item.collapsed = bool(_collapsed.get(sub.get_path(), true)) - - -func _find_tree_item(item: TreeItem, path: String) -> TreeItem: - var want := path.trim_suffix("/") - if str(item.get_metadata(0)).trim_suffix("/") == want: - return item - for c in item.get_children(): - var r := _find_tree_item(c, path) - if r: - return r - return null - - -func _on_tree_selected() -> void: - if _syncing_tree: - return - var item := _tree.get_selected() - if item: - var p := str(item.get_metadata(0)) - if _active_set.is_empty() and p.trim_suffix("/") == current_dir.trim_suffix("/"): - return - _navigate.call_deferred(p) - - -# ---------- Tree: pinned (sticky) items, single click, tools ---------- - -func _add_tree_tool_button(icon_name: String, fallback: String, tip: String, cb: Callable) -> void: - var b := Button.new() - b.flat = true - b.focus_mode = Control.FOCUS_NONE - b.tooltip_text = tip - var base := EditorInterface.get_base_control() - if base.has_theme_icon(icon_name, "EditorIcons"): - b.icon = base.get_theme_icon(icon_name, "EditorIcons") - else: - b.text = fallback - b.pressed.connect(cb) - _tree_tools.add_child(b) - - -func _setup_sticky() -> void: - # Overlay placed on the tree: shows the parent folders "stuck" at the top. - # Home-made implementation (works from Godot 4.2, without relying on 4.8's sticky Tree). - _sticky_box = Control.new() - _sticky_box.mouse_filter = Control.MOUSE_FILTER_IGNORE - # Safety net: whatever happens (late signature, shaky row measurement on the - # first display, etc.), the overlay must NEVER be able to draw outside the tree's - # rectangle — neither above nor below. - _sticky_box.clip_contents = true - _sticky_box.visible = false - _tree.clip_contents = true - _tree.add_child(_sticky_box) - for c in _tree.get_children(true): - if c is VScrollBar: - _tree_vscroll = c - break - - -func _process(_delta: float) -> void: - if _tree != null and _tree.is_visible_in_tree(): - _update_sticky() - _update_paste_pop_hover() - _update_copy_pop_hover() - _pump_svg_queue() - # Safeguard: the "Ctrl+Space" pill must always be invisible while the drawer - # is open. Reasserted every frame rather than relying only on open()/close(), - # in case a side path (theme reload, deferred call...) missed it. - if SHOW_HINT and is_instance_valid(_hint) and _hint.visible == is_open: - _hint.visible = not is_open - - -# Height of a tree row, measured by finding two consecutive row changes. -func _get_row_pitch() -> float: - if _row_pitch > 0.0: - return _row_pitch - var last: TreeItem = _tree.get_item_at_position(Vector2(4.0, 0.0)) - if last == null: - return 0.0 - var edges: Array[int] = [] - for y in range(1, 200): - var it: TreeItem = _tree.get_item_at_position(Vector2(4.0, float(y))) - if it == null: - break - if it != last: - edges.append(y) - last = it - if edges.size() == 2: - break - if edges.size() == 2 and edges[1] - edges[0] >= 8: - _row_pitch = float(edges[1] - edges[0]) - return _row_pitch - - -func _item_depth(item: TreeItem) -> int: - var d := 0 - var p := item.get_parent() - while p != null: - d += 1 - p = p.get_parent() - return d - - -func _row_index(target: TreeItem) -> int: - var idx := 0 - var it: TreeItem = _tree.get_root() - while it != null: - if it == target: - return idx - idx += 1 - it = it.get_next_visible() - return -1 - - -# List (root -> deep) of the folders to pin at the top of the tree. -func _compute_sticky_chain() -> Array: - var chain: Array = [] - if _tree.get_root() == null or _tree.get_scroll().y < 1.0: - return chain - var pitch := _get_row_pitch() - if pitch <= 0.0: - return chain - var slot := _sticky_row_h(pitch) # = pitch: we probe at multiples of a WHOLE row - var k := 0 - for _i in range(STICKY_MAX + 1): - # Row located just below the rows already pinned - var probe: TreeItem = _tree.get_item_at_position(Vector2(4.0, k * slot + 1.0)) - if probe == null: - break - var cand: Array = [] - var p := probe.get_parent() - while p != null: - cand.push_front(p) - p = p.get_parent() - if cand.size() > STICKY_MAX: - cand = cand.slice(cand.size() - STICKY_MAX) - chain = cand - if cand.size() == k: - break - k = cand.size() - return chain - - -func _update_sticky() -> void: - if _sticky_box == null: - return - var pitch := _get_row_pitch() - if _pending_reveal != null: - var pr := _pending_reveal - if pitch > 0.0: - _pending_reveal = null - if is_instance_valid(pr): - _reveal_tree_item(pr) - # Immediate, priority hiding: as soon as the tree is at the very top, there is no parent folder - # to show. We never depend on the signature cache for this specific case, to avoid - # a leftover of the overlay staying visible for a moment above "res://". (The handling of - # _pending_reveal above must stay BEFORE this test: it is what can move the - # scroll and therefore change this result on the next call.) - if _tree.get_scroll().y < 1.0: - if _sticky_box.visible: - _sticky_box.visible = false - _sticky_sig = "" - return - var chain := _compute_sticky_chain() - var w := _tree.size.x - if _tree_vscroll != null and _tree_vscroll.visible: - w -= _tree_vscroll.size.x - var sig := "%d#%s#%d" % [int(w), current_dir, int(pitch)] - for it in chain: - sig += "|" + str(it.get_metadata(0)) - if sig == _sticky_sig: - return - _sticky_sig = sig - _rebuild_sticky(chain, w, pitch) - - -## RESERVED height per pinned row: always one whole tree row. -## Never reduce this value: it is what guarantees that the pinned-folders overlay -## covers exactly a whole number of real rows, -## otherwise a real row appears half under the overlay (overlap). -func _sticky_row_h(pitch: float) -> float: - return pitch - - -func _rebuild_sticky(chain: Array, w: float, pitch: float) -> void: - for c in _sticky_box.get_children(): - _sticky_box.remove_child(c) - c.queue_free() - if chain.is_empty() or pitch <= 0.0: - _sticky_box.visible = false - return - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - var margin := maxi(_tree.get_theme_constant("item_margin"), 8) - var slot := _sticky_row_h(pitch) # = pitch: one reserved row = one whole tree row - # Explicit placement, one WHOLE row after the other: the overlay then covers exactly - # the same number of pixels as the real rows it hides, no row can - # end up half visible under the overlay (overlap). - var y := 0.0 - for it in chain: - var r := _make_sticky_row(it, pitch, margin, accent) - _sticky_box.add_child(r) - r.position = Vector2(0.0, y) - r.size = Vector2(w, slot) - y += slot - var line := ColorRect.new() - line.color = Color(accent.r, accent.g, accent.b, 0.55) - line.mouse_filter = Control.MOUSE_FILTER_IGNORE - _sticky_box.add_child(line) - line.position = Vector2(0.0, y) - line.size = Vector2(w, 1.0) - _sticky_box.position = Vector2.ZERO - _sticky_box.size = Vector2(w, y + 1.0) - _sticky_box.visible = true - - -func _sticky_row_style(bg: Color) -> StyleBoxFlat: - var s := StyleBoxFlat.new() - s.bg_color = bg - s.border_width_bottom = 1 - s.border_color = Color(0, 0, 0, 0.35) - return s - - -func _make_sticky_row(it: TreeItem, pitch: float, margin: int, accent: Color) -> Control: - var path := str(it.get_metadata(0)) - var depth := _item_depth(it) - var slot := _sticky_row_h(pitch) # RESERVED height = one whole row (alignment) - var is_cur := path.trim_suffix("/") == current_dir.trim_suffix("/") - var bg := _hover_c(COLOR_LEFT_PANEL) - if is_cur: - bg = bg.lerp(accent, 0.30) - bg.a = 1.0 # always opaque: this row must fully hide the real tree behind it - var hover_bg := _hover_c(bg) - hover_bg.a = 1.0 - var sb_normal := _sticky_row_style(bg) - var sb_hover := _sticky_row_style(hover_bg) - - # PanelContainer: its "panel" style paints the whole rectangle ALONE (the full - # reserved height, without the slightest transparent gap), and it automatically centers its - # content — no more manual anchoring math, so no possible offset. - var row := PanelContainer.new() - row.custom_minimum_size = Vector2(0, slot) - row.clip_contents = true - row.mouse_filter = Control.MOUSE_FILTER_PASS # the mouse wheel keeps going to the tree - row.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND - row.tooltip_text = path - row.add_theme_stylebox_override("panel", sb_normal) - row.mouse_entered.connect(func() -> void: row.add_theme_stylebox_override("panel", sb_hover)) - row.mouse_exited.connect(func() -> void: row.add_theme_stylebox_override("panel", sb_normal)) - - # Inner margins: shift the content without ever letting the background show through - # (they stay INSIDE the opaque panel above). - var pad := MarginContainer.new() - pad.add_theme_constant_override("margin_left", depth * margin + 4) - pad.add_theme_constant_override("margin_right", 6) - pad.add_theme_constant_override("margin_top", 0) - pad.add_theme_constant_override("margin_bottom", 0) - pad.mouse_filter = Control.MOUSE_FILTER_IGNORE - row.add_child(pad) - - var hb := HBoxContainer.new() - hb.add_theme_constant_override("separation", 4) - hb.mouse_filter = Control.MOUSE_FILTER_IGNORE - pad.add_child(hb) - - var arrow := TextureRect.new() - arrow.custom_minimum_size = Vector2(margin, 0) - arrow.expand_mode = TextureRect.EXPAND_KEEP_SIZE - arrow.stretch_mode = TextureRect.STRETCH_KEEP_CENTERED - arrow.size_flags_vertical = Control.SIZE_SHRINK_CENTER - arrow.mouse_filter = Control.MOUSE_FILTER_IGNORE - if _tree.has_theme_icon("arrow", "Tree"): - arrow.texture = _tree.get_theme_icon("arrow", "Tree") - hb.add_child(arrow) - - var icon := TextureRect.new() - icon.texture = it.get_icon(0) - icon.modulate = it.get_icon_modulate(0) - var isz := clampf(slot - 6.0, 10.0, 20.0) - icon.custom_minimum_size = Vector2(isz, isz) - icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER - icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - hb.add_child(icon) - - var lbl := Label.new() - lbl.text = it.get_text(0) - lbl.clip_text = true - lbl.add_theme_font_size_override("font_size", 12) - lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL - # EXPLICIT centering: the Label receives the whole row height (SIZE_FILL) and centers - # its text inside it (vertical_alignment), rather than depending on the "minimum size" - # computed by Godot (unreliable depending on font/DPI). - lbl.size_flags_vertical = Control.SIZE_FILL - lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE - hb.add_child(lbl) - - var arrow_end := float(depth * margin + margin + 2) - row.gui_input.connect(func(ev: InputEvent) -> void: - if ev is InputEventMouseButton: - var mb := ev as InputEventMouseButton - if mb.button_index in [MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_WHEEL_LEFT, MOUSE_BUTTON_WHEEL_RIGHT]: - return - row.accept_event() - if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: - if mb.position.x < arrow_end: - _on_sticky_arrow(path) - else: - _on_sticky_clicked(path) - elif ev is InputEventMouseMotion: - row.accept_event() - ) - return row - - -func _sticky_item(path: String) -> TreeItem: - var v: Variant = _tree_items.get(path.trim_suffix("/"), null) - if v != null and is_instance_valid(v): - return v as TreeItem - return null - - -# Click on a pinned folder: opens the folder and brings it just under its pinned parents -func _on_sticky_clicked(path: String) -> void: - if path.trim_suffix("/") != current_dir.trim_suffix("/"): - _navigate.call_deferred(path) - var it := _sticky_item(path) - if it != null: - _reveal_tree_item.call_deferred(it) - - -# Click on the arrow of a pinned folder: collapses it -func _on_sticky_arrow(path: String) -> void: - var it := _sticky_item(path) - if it == null: - return - it.collapsed = true - _reveal_tree_item.call_deferred(it) - - -# Makes the item visible, taking into account the pinned rows that would cover it -func _reveal_tree_item(item: TreeItem) -> void: - if item == null or not is_instance_valid(item) or _tree == null: - return - _tree.scroll_to_item(item) - if _tree_vscroll == null: - return - var pitch := _get_row_pitch() - if pitch <= 0.0: - _pending_reveal = item # tree not measurable yet: we try again later - return - var idx := _row_index(item) - if idx < 0: - return - var need := mini(_item_depth(item), STICKY_MAX) * _sticky_row_h(pitch) - var y_top := idx * pitch - _tree.get_scroll().y - if y_top < need - 0.5: - _tree_vscroll.value = maxf(0.0, _tree_vscroll.value - (need - y_top)) - - -# Single click on a tree folder: navigation (through the selection) + expansion -func _on_tree_gui_input(ev: InputEvent) -> void: - if not (ev is InputEventMouseButton): - return - var mb := ev as InputEventMouseButton - if mb.button_index != MOUSE_BUTTON_LEFT or not mb.pressed or mb.double_click: - return - var item := _tree.get_item_at_position(mb.position) - if item == null: - return - var path := str(item.get_metadata(0)) - var was_current := path.trim_suffix("/") == current_dir.trim_suffix("/") - _after_tree_click.call_deferred(item, item.collapsed, was_current) - - -func _after_tree_click(item: TreeItem, was_collapsed: bool, was_current: bool) -> void: - if not is_instance_valid(item) or item.collapsed != was_collapsed: - return # click on the arrow: already handled by Godot - var path := str(item.get_metadata(0)) - if item.get_first_child() != null: - if was_current: - item.collapsed = not was_collapsed # re-click on the current folder: toggle - elif was_collapsed: - item.collapsed = false # new folder: we expand it - _center_tree_item_soon(path) - - -# Waits for the tree to finish its layout (expansion, selection) then centers the clicked folder -func _center_tree_item_soon(path: String) -> void: - await get_tree().process_frame - await get_tree().process_frame - _center_tree_item(_sticky_item(path)) - - -# Scrolls the tree (smoothly) so that the item is in the middle of the visible area -func _center_tree_item(item: TreeItem) -> void: - if item == null or not is_instance_valid(item) or _tree_vscroll == null: - return - if not _tree.is_visible_in_tree(): - return - var pitch := _get_row_pitch() - if pitch <= 0.0: - return - var idx := _row_index(item) - if idx < 0: - return - # Space reserved for the pinned parent folders of THIS item (same rule as the - # sticky overlay: always a whole number of rows). If we ignore it here, scrolling can bring - # the item just under the overlay with a fraction-of-a-row offset, which makes - # a real row appear half visible UNDER the overlay (the overlap bug). - var reserved := mini(_item_depth(item), STICKY_MAX) * _sticky_row_h(pitch) - var available := maxf(_tree.size.y - reserved, pitch) - # Number of context rows to leave visible above the item, in half of the - # space available under the overlay — always a WHOLE number of rows to stay aligned. - var context_rows := mini(idx, floori((available * 0.5) / pitch)) - var target := (idx - context_rows) * pitch - reserved - var max_v := maxf(0.0, _tree_vscroll.max_value - _tree_vscroll.page) - target = clampf(target, 0.0, max_v) - if _center_tween != null and _center_tween.is_valid(): - _center_tween.kill() - _center_tween = create_tween() - _center_tween.tween_property(_tree_vscroll, "value", target, 0.18) \ - .set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) - - -func _collapse_all_folders() -> void: - var root := _tree.get_root() - if root == null: - return - for c in root.get_children(): - _collapse_rec(c) - _sync_tree_selection() - - -func _collapse_rec(it: TreeItem) -> void: - for c in it.get_children(): - _collapse_rec(c) - if not it.collapsed: - it.collapsed = true - - -func _set_preview_tex(tex: Texture2D) -> void: - _preview.texture = tex - for sh in _preview_shadows: - sh.texture = tex - - -## Size (px) at which to rasterize SVGs, depending on zoom and editor scale -## (≈ 2× the displayed size: the ItemList then downscales without aliasing). -func _svg_target() -> int: - var base_px := 128 - match _icon_bucket: - 24: base_px = 64 - 64: base_px = 128 - 96: base_px = 192 - 160: base_px = 320 - var ui_scale := clampf(EditorInterface.get_editor_scale(), 1.0, 2.0) - return mini(int(base_px * ui_scale), 512) - - -func _pump_svg_queue() -> void: - if _svg_queue.is_empty(): - return - var t0 := Time.get_ticks_usec() - while not _svg_queue.is_empty() and Time.get_ticks_usec() - t0 < 6000: - var job: Array = _svg_queue.pop_front() - var path: String = job[0] - var target: int = job[1] - var tex := _render_svg(path, target) - if tex != null: - _svg_px[path] = target - _on_preview(path, tex, null, "svg") - else: - _svg_fail[path] = true - _preview_pending.erase(path) - _schedule_previews() # falls back to Godot's thumbnail - - -func _render_svg(path: String, target: int) -> Texture2D: - var text := FileAccess.get_file_as_string(path) - if text.is_empty(): - return null - var img := Image.new() - if img.load_svg_from_string(text, 1.0) != OK or img.get_width() <= 0 or img.get_height() <= 0: - return null - var longest := maxi(img.get_width(), img.get_height()) - if absf(float(longest) - float(target)) > 2.0: - # Direct rendering at the right scale (vector: no loss, unlike an enlargement) - var img2 := Image.new() - if img2.load_svg_from_string(text, float(target) / float(longest)) == OK and img2.get_width() > 0: - img = img2 - return ImageTexture.create_from_image(img) - - -func _on_preview(path: String, preview: Texture2D, _thumb: Texture2D, _user) -> void: - _preview_pending.erase(path) - if str(_user) != "svg" and _svg_px.has(path) and _preview_cache.get(path, null) != null: - return # we keep our own rasterization, which is sharper - if _preview_cache.size() > 4000: - _preview_cache.clear() - _preview_cache[path] = preview # null = failure, avoids asking again in a loop - if preview == null or _list == null: - return - if _path_index.has(path): - var i: int = _path_index[path] - if i < _list.item_count and str(_list.get_item_metadata(i)) == path: - _list.set_item_icon(i, preview) - if _context_path == path and _list.get_selected_items().size() <= 1: - _set_preview_tex(preview) - - -# ---------- Animated opening / closing ---------- - -func toggle() -> void: - if is_open: - close() - elif Time.get_ticks_msec() - _outside_close_msec > 250: - # Avoids reopening right away when the click that just closed the drawer - # (outside click) lands on the toolbar button. - open() - - -func open() -> void: - var base := EditorInterface.get_base_control() - _sync_favorites() - is_open = true - _set_hint_visible(false) - _layout(base) - position.y = base.size.y + 20.0 - modulate.a = 0.0 - scale = Vector2(0.965, 0.935) - pivot_offset = Vector2(size.x * 0.5, size.y) - visible = true - if _dirty: - _refresh() - else: - _schedule_previews() - _animate_open(base) - _list.grab_focus() - - -func close() -> void: - if not is_open: - return - is_open = false - _flush_cfg() - _ctx_popup.hide() - var base := EditorInterface.get_base_control() - _animate_close(base) - _tween.finished.connect(func() -> void: - if not is_open: - visible = false - scale = Vector2.ONE - _set_hint_visible(true) - , CONNECT_ONE_SHOT) - - -func _open_y(base: Control) -> float: - return base.size.y - size.y - BOTTOM_GAP - - -func _layout(base: Control) -> void: - var h := clampf(base.size.y * _height_ratio, MIN_HEIGHT, base.size.y * 0.85) - var w := maxf(minf(base.size.x - SIDE_GAP * 2.0, MAX_WIDTH), 320.0) - size = Vector2(w, h) - position.x = (base.size.x - size.x) * 0.5 - position.y = _open_y(base) if is_open else base.size.y + 20.0 - - -func _on_editor_resized() -> void: - _layout_hint() - if is_inside_tree() and is_open: - _layout(EditorInterface.get_base_control()) - - -# ---------- Hint pill (shortcut) ---------- - -func _build_hint(base: Control) -> void: - var accent: Color = base.get_theme_color("accent_color", "Editor") - _hint = PanelContainer.new() - _hint.name = "AssetDrawerHint" - _hint.z_index = 200 # above the drawer panel itself (z_index = 128) - _hint.z_as_relative = false - _hint.modulate.a = 0.65 - _hint.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND - _hint.tooltip_text = L.t("Cliquer pour ouvrir le tiroir d'assets") - var hint_style := _make_stylebox(COLOR_HEADER, Color(accent.r, accent.g, accent.b, 0.7), 8, 1) - _hint.add_theme_stylebox_override("panel", hint_style) - var pad := _pad(10, 4) - _hint.add_child(pad) - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 8) - pad.add_child(row) - var title := Label.new() - title.text = "Asset Drawer" - title.add_theme_font_override("font", base.get_theme_font("bold", "EditorFonts")) - title.add_theme_font_size_override("font_size", 12) - row.add_child(title) - var key := PanelContainer.new() - key.add_theme_stylebox_override("panel", _make_stylebox(_ov(0.06), _ov(0.25), 4, 1)) - var key_pad := _pad(6, 1) - key.add_child(key_pad) - var key_lbl := Label.new() - key_lbl.text = L.t("Ctrl+Espace") - key_lbl.modulate = Color(1, 1, 1, 0.8) - key_lbl.add_theme_font_size_override("font_size", 11) - key_pad.add_child(key_lbl) - row.add_child(key) - _ignore_mouse(pad) - - _hint.mouse_entered.connect(func() -> void: _hint.modulate.a = 1.0) - _hint.mouse_exited.connect(func() -> void: _hint.modulate.a = 0.65) - _hint.gui_input.connect(func(e: InputEvent) -> void: - var mb := e as InputEventMouseButton - if mb != null and mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT: - open() - ) - base.add_child(_hint) - _layout_hint.call_deferred() - _breathe_hint(hint_style, accent) - - -## Slow, discreet breathing of the hint pill's outline, looping, -## to gently signal that it is clickable without being loud. -func _breathe_hint(style: StyleBoxFlat, accent: Color) -> void: - if _hint_tween and _hint_tween.is_valid(): - _hint_tween.kill() - _hint_tween = create_tween().set_loops() - _hint_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - _hint_tween.tween_property(style, "border_color", Color(accent.r, accent.g, accent.b, 0.95), 1.4) - _hint_tween.tween_property(style, "border_color", Color(accent.r, accent.g, accent.b, 0.45), 1.4) - - -func _ignore_mouse(node: Node) -> void: - if node is Control: - (node as Control).mouse_filter = Control.MOUSE_FILTER_IGNORE - for c in node.get_children(): - _ignore_mouse(c) - - -func _layout_hint() -> void: - if not is_instance_valid(_hint): - return - var base := EditorInterface.get_base_control() - var sz := _hint.get_combined_minimum_size() - _hint.size = sz - _hint.position = Vector2((base.size.x - sz.x) * 0.5, base.size.y - sz.y - HINT_BOTTOM) - - -func _set_hint_visible(on: bool) -> void: - if is_instance_valid(_hint): - _hint.visible = on - if on: - _layout_hint() - - -## Opening with a slight bounce ("overshoot"): livelier than a plain linear slide. -func _animate_open(base: Control) -> void: - if _tween: - _tween.kill() - pivot_offset = Vector2(size.x * 0.5, size.y) - var dur := ANIM_TIME * 1.4 - _tween = create_tween().set_parallel(true) - _tween.tween_property(self, "position:y", _open_y(base), dur) \ - .set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) - _tween.tween_property(self, "scale", Vector2.ONE, dur) \ - .set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) - _tween.tween_property(self, "modulate:a", 1.0, dur * 0.55) \ - .set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - _tween.chain().tween_callback(_pulse_glow) - - -## Shorter, crisper closing, with a slight fold-back to accompany the exit. -func _animate_close(base: Control) -> void: - if _tween: - _tween.kill() - if _glow_tween and _glow_tween.is_valid(): - _glow_tween.kill() - var dur := ANIM_TIME * 0.85 - _tween = create_tween().set_parallel(true).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN) - _tween.tween_property(self, "position:y", base.size.y + 20.0, dur) - _tween.tween_property(self, "scale", Vector2(0.972, 0.955), dur) - _tween.tween_property(self, "modulate:a", 0.0, dur * 0.85) - - -## Pulse of the accent outline/shadow right after opening: a discreet luminous "pop". -func _pulse_glow() -> void: - if _panel_style == null or not is_open: - return - var accent: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - if _glow_tween and _glow_tween.is_valid(): - _glow_tween.kill() - _panel_style.shadow_size = 12 - _glow_tween = create_tween() - _glow_tween.set_trans(Tween.TRANS_SINE) - _glow_tween.tween_property(_panel_style, "shadow_color", Color(accent.r, accent.g, accent.b, 0.35), 0.15).set_ease(Tween.EASE_OUT) - _glow_tween.parallel().tween_property(_panel_style, "shadow_size", 16.0, 0.15).set_ease(Tween.EASE_OUT) - _glow_tween.tween_property(_panel_style, "shadow_color", Color(0, 0, 0, 0.30), 0.6).set_ease(Tween.EASE_IN) - _glow_tween.parallel().tween_property(_panel_style, "shadow_size", 12.0, 0.6).set_ease(Tween.EASE_IN) - - -func _on_grip_input(event: InputEvent) -> void: - if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: - _resizing = event.pressed - if not event.pressed: - _save_cfg() - elif event is InputEventMouseMotion and _resizing: - var base := EditorInterface.get_base_control() - var new_h: float = base.size.y - BOTTOM_GAP - (position.y + event.relative.y) - _height_ratio = clampf(new_h / base.size.y, 0.04, 0.85) - _layout(base) - - -# ---------- Navigation ---------- - -func _navigate(path: String) -> void: - var dir := path if DirAccess.dir_exists_absolute(path) else path.get_base_dir() - if dir == "res:": - dir = "res://" - current_dir = dir - _active_set = "" - _search.text = "" - if _history_i < _history.size() - 1: - _history.resize(_history_i + 1) - if _history[_history_i] != current_dir: - _history.append(current_dir) - _history_i = _history.size() - 1 - _refresh() - _save_cfg() - - -func _hist_go(delta: int) -> void: - var ni := _history_i + delta - if ni >= 0 and ni < _history.size(): - _history_i = ni - current_dir = _history[_history_i] - _active_set = "" - _search.text = "" - _refresh() - - -func _go_up() -> void: - if not _active_set.is_empty(): - _active_set = "" - _refresh() - elif current_dir != "res://": - _navigate(current_dir.trim_suffix("/").get_base_dir()) - - -func _show_in_dock(path: String) -> void: - var dock := EditorInterface.get_file_system_dock() - if dock and dock.has_method("navigate_to_path"): - dock.navigate_to_path(path) - - -func _on_item_activated(index: int) -> void: - _open_path(str(_list.get_item_metadata(index))) - - -const MODEL_EXTS := ["glb", "gltf", "fbx", "blend", "dae", "obj"] -const MODEL_SCENE_EXTS := ["glb", "gltf", "fbx", "blend", "dae"] # imported as scenes (obj = Mesh) - - -func _is_model(path: String) -> bool: - return path.get_extension().to_lower() in MODEL_EXTS - - -func _is_model_scene(path: String) -> bool: - return path.get_extension().to_lower() in MODEL_SCENE_EXTS - - -## Selects the file (the Import dock updates) then brings the Import tab to the front. -func _show_import_settings(path: String) -> void: - EditorInterface.select_file(path) - get_tree().create_timer(0.08).timeout.connect(_focus_import_dock) - if not pinned: - close() - - -func _focus_import_dock() -> void: - var dock := _find_node_by_class(EditorInterface.get_base_control(), "ImportDock") - if dock == null: - return - var child: Node = dock - var parent: Node = dock.get_parent() - while parent != null and not (parent is TabContainer): - child = parent - parent = parent.get_parent() - if parent is TabContainer and child is Control: - var tc := parent as TabContainer - var idx := tc.get_tab_idx_from_control(child as Control) - if idx >= 0: - tc.current_tab = idx - - -func _find_node_by_class(n: Node, cls: String) -> Node: - if n.get_class() == cls: - return n - for c in n.get_children(): - var r := _find_node_by_class(c, cls) - if r != null: - return r - return null - - -## Opens Godot's "Advanced Import" window: selects the file, waits for the Import dock -## to show that file, then triggers its "Advanced..." button. -func _open_advanced_import(path: String) -> void: - if not _is_model_scene(path): - _show_import_settings(path) # e.g. .obj: no advanced import, we show the Import dock - return - EditorInterface.select_file(path) - if not pinned: - close() - _try_advanced_import(path, 0) - - -func _try_advanced_import(path: String, attempt: int) -> void: - await get_tree().create_timer(0.15).timeout - # 1) Main method: reproduce exactly the double-click in the FileSystem dock, which itself - # calls Godot's advanced import for 3D models. - if _activate_in_filesystem_dock(path): - return - # 2) Fallback: the "Advanced..." button of the Import dock. - var dock := _find_node_by_class(EditorInterface.get_base_control(), "ImportDock") - if dock != null and _import_dock_shows(dock, path): - var btn := _find_advanced_button(dock) - if btn != null: - btn.pressed.emit() - return - if attempt < 12: - _try_advanced_import(path, attempt + 1) - else: - _focus_import_dock() - _flash(L.t("Import avancé indisponible pour ce fichier"), true) - - -## Simulates a double-click on the file in the FileSystem dock (file list or tree). -func _activate_in_filesystem_dock(path: String) -> bool: - var fsd: Node = EditorInterface.get_file_system_dock() - if fsd == null: - return false - return _activate_in_node(fsd, path) - - -func _activate_in_node(n: Node, path: String) -> bool: - if n is ItemList: - var il := n as ItemList - for i in il.item_count: - var m: Variant = il.get_item_metadata(i) - if (m is String or m is StringName) and str(m) == path: - il.select(i) - il.item_activated.emit(i) - return true - elif n is Tree: - var t := n as Tree - var root := t.get_root() - if root != null: - var it := _find_fs_tree_item(root, path) - if it != null: - it.select(0) - t.item_activated.emit() - return true - for c in n.get_children(): - if _activate_in_node(c, path): - return true - return false - - -func _find_fs_tree_item(item: TreeItem, path: String) -> TreeItem: - var m: Variant = item.get_metadata(0) - if (m is String or m is StringName) and str(m) == path: - return item - for c in item.get_children(): - var r := _find_fs_tree_item(c, path) - if r != null: - return r - return null - - -func _import_dock_shows(n: Node, path: String) -> bool: - if n is Label and (n as Label).text.contains(path.get_file()): - return true - for c in n.get_children(): - if _import_dock_shows(c, path): - return true - return false - - -func _find_advanced_button(root: Node) -> Button: - var buttons: Array[Button] = [] - _collect_buttons(root, buttons) - for b in buttons: - var t := b.text.strip_edges() - if b.visible and not b.disabled and (t.begins_with("Advanced") or t.begins_with("Avanc")): - return b - # Unknown language: the "Advanced..." button is the only plain button whose text ends with "..." - for b in buttons: - var t := b.text.strip_edges() - if b.visible and not b.disabled and b.get_class() == "Button" and (t.ends_with("...") or t.ends_with("…")): - return b - return null - - -func _collect_buttons(n: Node, out: Array[Button]) -> void: - if n is Button: - out.append(n as Button) - for c in n.get_children(): - _collect_buttons(c, out) - - -## Opens the model as a scene: Godot then offers to create a new inherited scene. -func _new_inherited_scene(path: String) -> void: - EditorInterface.open_scene_from_path(path) - if not pinned: - close() - - -func _open_path(path: String) -> void: - if path.is_empty(): - return - if DirAccess.dir_exists_absolute(path): - _navigate(path) - return - _push_recent(path) - var ext := path.get_extension().to_lower() - if ext in ["tscn", "scn"]: - EditorInterface.open_scene_from_path(path) - elif _is_model(path): - # Opens the model's "Advanced Import" window (creating an inherited scene - # is in the right-click menu). - _open_advanced_import(path) - elif ResourceLoader.exists(path): - var res := load(path) - if res is Script: - EditorInterface.edit_script(res) - elif res is PackedScene: - EditorInterface.open_scene_from_path(path) # .glb / .gltf / .fbx imported as scenes - else: - EditorInterface.edit_resource(res) - else: - OS.shell_open(ProjectSettings.globalize_path(path)) - return - if not pinned: - close() - - -# ---------- Keyboard & mouse ---------- - -func _input(event: InputEvent) -> void: - if _ctx_popup != null and _ctx_popup.visible: - if event is InputEventMouseButton and event.pressed: - if not _ctx_popup.get_global_rect().has_point((event as InputEventMouseButton).global_position): - _ctx_popup.hide() - get_viewport().set_input_as_handled() - return - elif event is InputEventKey and event.pressed and (event as InputEventKey).keycode == KEY_ESCAPE: - _ctx_popup.hide() - get_viewport().set_input_as_handled() - return - # Mouse side buttons: back / forward (only over the drawer) - if is_open and event is InputEventMouseButton and event.pressed: - var xb := event as InputEventMouseButton - if xb.button_index == MOUSE_BUTTON_XBUTTON1 or xb.button_index == MOUSE_BUTTON_XBUTTON2: - if get_global_rect().has_point(xb.global_position): - _hist_go(-1 if xb.button_index == MOUSE_BUTTON_XBUTTON1 else 1) - get_viewport().set_input_as_handled() - return - # Click outside the drawer -> closes it (like Unreal's Content Drawer), unless pinned. - # The event is not consumed: the click keeps its effect on whatever was clicked. - if is_open and not pinned and event is InputEventMouseButton and event.pressed: - var mb := event as InputEventMouseButton - var is_wheel := mb.button_index in [MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_WHEEL_LEFT, MOUSE_BUTTON_WHEEL_RIGHT, MOUSE_BUTTON_XBUTTON1, MOUSE_BUTTON_XBUTTON2] - if not is_wheel and not _resizing and not _ctx_popup.visible and not get_global_rect().has_point(mb.global_position): - _outside_close_msec = Time.get_ticks_msec() - close() - return - if not is_open or not (event is InputEventKey) or not event.pressed or event.echo: - return - var k := event as InputEventKey - var focus := get_viewport().gui_get_focus_owner() - if focus != null and not is_ancestor_of(focus): - return - if k.ctrl_pressed and k.keycode == KEY_F: - _search.grab_focus() - _search.select_all() - get_viewport().set_input_as_handled() - return - if focus is LineEdit or focus is TextEdit: - return - # Actions on items: only if the list has focus (not the tree nor a button) - if focus != null and focus != _list and k.keycode != KEY_BACKSPACE: - return - var sel := _selected_paths() - var handled := true - match k.keycode: - KEY_F2: - if sel.size() == 1: - _rename(sel[0]) - KEY_DELETE: - if not sel.is_empty(): - _confirm_delete(sel) - KEY_D: - if k.ctrl_pressed and not sel.is_empty(): - _duplicate(sel) - else: - handled = false - KEY_BACKSPACE: - _go_up() - KEY_ENTER, KEY_KP_ENTER: - if sel.size() == 1: - _open_path(sel[0]) - _: - handled = false - if handled: - get_viewport().set_input_as_handled() - - -func _on_list_gui_input(event: InputEvent) -> void: - # --- Rectangle selection (left click held in an empty area) --- - if event is InputEventMouseMotion and _mq_active: - var mm := event as InputEventMouseMotion - _marquee_update(mm.position, mm.button_mask) - return - if event is InputEventMouseButton and not event.pressed and _mq_active \ - and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT: - _marquee_end() - return - if not (event is InputEventMouseButton) or not event.pressed: - return - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT and not mb.double_click \ - and _list.item_count > 0 and _list.get_item_at_position(mb.position, true) < 0: - _marquee_begin(mb) - elif mb.button_index == MOUSE_BUTTON_RIGHT: - var idx := _list.get_item_at_position(mb.position, true) - if idx >= 0: - if not _list.is_selected(idx): - _list.deselect_all() - _list.select(idx, true) - _on_selection_changed() - _context_path = str(_list.get_item_metadata(idx)) - else: - _list.deselect_all() - _on_selection_changed() - _context_path = current_dir - _show_context() - _list.accept_event() - elif mb.ctrl_pressed and (mb.button_index == MOUSE_BUTTON_WHEEL_UP or mb.button_index == MOUSE_BUTTON_WHEEL_DOWN): - var zstep := 4.0 if _view_list else 8.0 - _zoom.value += zstep if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -zstep - _list.accept_event() - - -func _marquee_begin(mb: InputEventMouseButton) -> void: - _mq_active = true - _mq_moved = false - var additive := mb.ctrl_pressed or mb.shift_pressed - _mq_base = _list.get_selected_items() if additive else PackedInt32Array() - _mq_start = Vector2(mb.position.x, mb.position.y + _list.get_v_scroll_bar().value) - _mq_cur = mb.position - - -func _marquee_update(pos: Vector2, mask: int) -> void: - if (mask & MOUSE_BUTTON_MASK_LEFT) == 0: - _marquee_end() - return - _mq_cur = pos - var vs := _list.get_v_scroll_bar() - if not _mq_moved: - if Vector2(_mq_start.x, _mq_start.y - vs.value).distance_to(pos) < 4.0: - return - _mq_moved = true - # Auto-scroll near the top / bottom edges - if pos.y < 14.0: - vs.value -= 18.0 - elif pos.y > _list.size.y - 14.0: - vs.value += 18.0 - var sv := Vector2(_mq_start.x, _mq_start.y - vs.value) - var rect := Rect2(sv, pos - sv).abs() - _list.deselect_all() - for i in _mq_base: - _list.select(i, false) - for i in _list.item_count: - var r := _item_rect(i, true).grow(-4.0) - if r.position.y > rect.end.y: - break - if r.end.y < rect.position.y: - continue - if r.intersects(rect): - _list.select(i, false) - _list.queue_redraw() - - -func _marquee_end() -> void: - var was_moved := _mq_moved - _mq_active = false - _mq_moved = false - _list.queue_redraw() - if was_moved: - _on_selection_changed() - - -func _get_drag_data_fw(_at: Vector2) -> Variant: - if _mq_active: - return null - var files := _selected_paths() - if files.is_empty(): - return null - var base := EditorInterface.get_base_control() - var accent: Color = base.get_theme_color("accent_color", "Editor") - var first: String = files[0] - var is_dir := DirAccess.dir_exists_absolute(first) - - # Small floating card (icon + name, slightly tilted) rather than a bare Label. - var wrap := Control.new() - var card := PanelContainer.new() - card.rotation = deg_to_rad(-3.0) - card.add_theme_stylebox_override("panel", _make_stylebox(COLOR_CARD_BG, Color(accent.r, accent.g, accent.b, 0.9), 7, 1)) - var pad := _pad(8, 5) - card.add_child(pad) - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 6) - pad.add_child(row) - var icon := TextureRect.new() - icon.custom_minimum_size = Vector2(18, 18) - icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - icon.texture = base.get_theme_icon("Folder" if is_dir else "File", "EditorIcons") - if is_dir: - icon.modulate = _folder_color(_colors(), first) - row.add_child(icon) - var lbl := Label.new() - lbl.text = first.trim_suffix("/").get_file() + (" (+%d)" % (files.size() - 1) if files.size() > 1 else "") - lbl.add_theme_font_size_override("font_size", 12) - row.add_child(lbl) - wrap.add_child(card) - set_drag_preview(wrap) - return {"type": "files", "files": files} - - -# ---------- Drag and drop: move files / folders into a folder ---------- - -func _notification(what: int) -> void: - if what == NOTIFICATION_DRAG_END: - _clear_drop_hl() - - -# Paths contained in a drag (drawer or Godot's FileSystem dock), without trailing "/" -func _drag_paths(data: Variant) -> PackedStringArray: - var out := PackedStringArray() - if data is Dictionary: - var t := str(data.get("type", "")) - if t == "files" or t == "files_and_dirs": - for f in data.get("files", []): - var p := str(f) - if p != "res://": - p = p.trim_suffix("/") - if not p.is_empty() and not out.has(p): - out.append(p) - return out - - -func _norm_dir(p: String) -> String: - if p == "res://" or p == "res:/": - return "res://" - return p.trim_suffix("/") - - -func _can_move_one(src: String, dst_dir: String) -> bool: - if src.is_empty() or src == "res://": - return false - if src.get_base_dir() == dst_dir or src == dst_dir: - return false - if _is_under(dst_dir, src): - return false # a folder cannot go into itself / one of its subfolders - return FileAccess.file_exists(src) or DirAccess.dir_exists_absolute(src) - - -func _can_move_any(files: PackedStringArray, dst_dir: String) -> bool: - for f in files: - if _can_move_one(f, dst_dir): - return true - return false - - -func _drop_hl_color() -> Color: - var acc: Color = EditorInterface.get_base_control().get_theme_color("accent_color", "Editor") - return Color(acc.r, acc.g, acc.b, 0.38) - - -func _set_drop_hl_list(idx: int, whole: bool = false) -> void: - if _list == null: - return - if idx == _drop_list_idx and whole == _drop_whole: - return - _drop_list_idx = idx - _drop_whole = whole - _list.queue_redraw() - - -func _set_drop_hl_tree(item: TreeItem) -> void: - if item == _drop_tree_item: - return - _drop_tree_item = item - if _tree != null: - _tree.queue_redraw() - - -func _clear_drop_hl() -> void: - _set_drop_hl_list(-1) - _set_drop_hl_tree(null) - - -# Target of a drop in the list: hovered folder, otherwise the current folder (empty area) -func _list_drop_target(at: Vector2) -> Dictionary: - var idx := _list.get_item_at_position(at, true) - if idx >= 0: - var p := str(_list.get_item_metadata(idx)) - if _dir_paths.has(p): - return {"dir": _norm_dir(p), "idx": idx} - # Empty area (or file): we drop into the current folder, except in search / set, - # where the list mixes several folders. - if not _active_set.is_empty() or not _search.text.strip_edges().is_empty(): - return {"dir": "", "idx": -1} - return {"dir": _norm_dir(current_dir), "idx": -1} - - -func _list_can_drop(at: Vector2, data: Variant) -> bool: - var files := _drag_paths(data) - if files.is_empty(): - _set_drop_hl_list(-1) - return false - var tgt := _list_drop_target(at) - var dst: String = tgt["dir"] - var ok := not dst.is_empty() and _can_move_any(files, dst) - _set_drop_hl_list(int(tgt["idx"]) if ok else -1, ok and int(tgt["idx"]) < 0) - return ok - - -func _list_drop(at: Vector2, data: Variant) -> void: - var files := _drag_paths(data) - var dst: String = _list_drop_target(at)["dir"] - _clear_drop_hl() - if dst.is_empty(): - return - _move_paths(files, dst) - - -func _tree_drop_dir(at: Vector2) -> String: - var it := _tree.get_item_at_position(at) - if it == null: - return "" - return _norm_dir(str(it.get_metadata(0))) - - -func _tree_can_drop(at: Vector2, data: Variant) -> bool: - var files := _drag_paths(data) - var it := _tree.get_item_at_position(at) - if files.is_empty() or it == null: - _set_drop_hl_tree(null) - return false - var ok := _can_move_any(files, _norm_dir(str(it.get_metadata(0)))) - _set_drop_hl_tree(it if ok else null) - return ok - - -func _tree_drop(at: Vector2, data: Variant) -> void: - var files := _drag_paths(data) - var dst := _tree_drop_dir(at) - _clear_drop_hl() - if dst.is_empty(): - return - _move_paths(files, dst) - - -# You can also grab a folder in the tree to move it into another folder -func _tree_get_drag(at: Vector2) -> Variant: - var it := _tree.get_item_at_position(at) - if it == null: - return null - var p := _norm_dir(str(it.get_metadata(0))) - if p == "res://": - return null - var lbl := Label.new() - lbl.text = " " + p.get_file() + " " - lbl.add_theme_stylebox_override("normal", _make_stylebox(COLOR_CARD_BG, _drop_hl_color(), 6, 1)) - set_drag_preview(lbl) - return {"type": "files", "files": PackedStringArray([p])} - - -func _move_paths(files: PackedStringArray, dst_dir: String) -> void: - EditorInterface.save_all_scenes() # <-- ADDED: avoids losing changes on reload - var moved := 0 - var moves: Array = [] # <-- ADDED - for p in files: - var src := p.trim_suffix("/") - if not _can_move_one(src, dst_dir): - continue - var dst := dst_dir.path_join(src.get_file()) - if FileAccess.file_exists(dst) or DirAccess.dir_exists_absolute(dst): - _flash(L.t("« %s » existe déjà") % src.get_file(), true) - continue - var is_dir := DirAccess.dir_exists_absolute(src) # <-- ADDED (before the rename) - if DirAccess.rename_absolute(src, dst) != OK: - _flash(L.t("Échec du déplacement"), true) - continue - moves.append([src, dst, is_dir]) # <-- ADDED - for ext in [".import", ".uid"]: - if FileAccess.file_exists(src + ext): - DirAccess.rename_absolute(src + ext, dst + ext) - _rewrite_paths(src, dst) - current_dir = _swap_prefix(current_dir, src, dst) - for i in _history.size(): - _history[i] = _swap_prefix(_history[i], src, dst) - moved += 1 - if moved == 0: - return - _update_references(moves) # <-- ADDED - _scan() - _flash(L.t("%d élément(s) déplacé(s)") % moved if moved > 1 else L.t("Élément déplacé")) - - -# ---------- Context menu ---------- - -func _ctx_btn(title: String, callback: Callable, color: Color = Color.TRANSPARENT, icon_name: String = "") -> Button: - var b := Button.new() - b.text = title - b.alignment = HORIZONTAL_ALIGNMENT_LEFT - _style_ghost_button(b, 8, 3) - if color != Color.TRANSPARENT: - b.add_theme_color_override("font_color", color) - if not icon_name.is_empty(): - var base := EditorInterface.get_base_control() - if base.has_theme_icon(icon_name, "EditorIcons"): - b.icon = base.get_theme_icon(icon_name, "EditorIcons") - b.add_theme_constant_override("h_separation", 8) - b.pressed.connect(func() -> void: - _ctx_popup.hide() - callback.call() - ) - _ctx_vbox.add_child(b) - return b - - -func _ctx_sep() -> void: - _ctx_vbox.add_child(HSeparator.new()) - - -## Checkbox menu entry. -func _ctx_check(title: String, checked: bool, on_toggle: Callable) -> CheckBox: - var cb := CheckBox.new() - cb.text = title - cb.alignment = HORIZONTAL_ALIGNMENT_LEFT - cb.focus_mode = Control.FOCUS_NONE - cb.button_pressed = checked - _style_ghost_button(cb, 8, 3) - # The theme colors the text of a checked button: we keep the normal color - var fc: Color = EditorInterface.get_base_control().get_theme_color("font_color", "Button") - for n in ["font_pressed_color", "font_hover_pressed_color", "font_focus_color"]: - cb.add_theme_color_override(n, fc) - cb.toggled.connect(func(on: bool) -> void: - _ctx_popup.hide() - on_toggle.call(on) - ) - _ctx_vbox.add_child(cb) - return cb - - -## Loads the resource in memory and keeps it as the drawer's "clipboard". -## A "Paste" button then appears when hovering any compatible resource field -## of the Inspector (or Alt+V as a keyboard shortcut). -func _copy_resource_to_clipboard(path: String) -> void: - if DirAccess.dir_exists_absolute(path): - _flash(L.t("Impossible de copier un dossier comme ressource"), true) - return - if not ResourceLoader.exists(path): - _flash(L.t("Ce fichier n'est pas une ressource Godot"), true) - return - var res := load(path) - if res == null or not (res is Resource): - _flash(L.t("Impossible de charger la ressource"), true) - return - _clip_resource = res - _clip_resource_name = path.get_file() - _flash(L.t("« %s » copiée — survolez un champ ressource de l'Inspecteur") % _clip_resource_name) - - -## True if `res` matches at least one of the types allowed by an EditorResourcePicker -## (native types via is_class, and custom script classes via their global class_name). -func _resource_type_matches(res: Resource, allowed: PackedStringArray) -> bool: - if allowed.is_empty(): - return true - var script := res.get_script() - var global_name := "" - if script is Script and (script as Script).has_method("get_global_name"): - global_name = (script as Script).get_global_name() - for t in allowed: - var tn: String = String(t).strip_edges() - if tn.is_empty(): - continue - if res.is_class(tn): - return true - if not global_name.is_empty() and global_name == tn: - return true - return false - - -## Walks up the parent chain from the hovered control to find an -## EditorResourcePicker (the widget of a resource field in the Inspector). -func _picker_under(control: Control) -> EditorResourcePicker: - var n: Node = control - while n: - if n is EditorResourcePicker: - return n - n = n.get_parent() - return null - - -func _picker_accepts_clip(picker: EditorResourcePicker) -> bool: - if picker == null or not is_instance_valid(picker) or _clip_resource == null: - return false - return picker.editable and _resource_type_matches(_clip_resource, picker.get_allowed_types()) - - -## Looks for an EditorResourcePicker (Inspector resource field) under the mouse -## and assigns the copied resource to it if the type matches. Alt+V keyboard shortcut (plugin.gd); -## the floating "Paste" button that appears on hover does the same on click. -func try_paste_resource_at_mouse() -> bool: - if _clip_resource == null or not is_instance_valid(_clip_resource): - _flash(L.t("Aucune ressource copiée : clic droit sur un fichier → « Copier la ressource »"), true) - return false - var vp := EditorInterface.get_base_control().get_viewport() - var hovered: Control = vp.gui_get_hovered_control() if vp else null - var picker := _picker_under(hovered) - if picker == null: - _flash(L.t("Survolez un champ ressource de l'Inspecteur puis Alt+V"), true) - return false - if not picker.editable: - _flash(L.t("Ce champ n'est pas modifiable"), true) - return false - var allowed := picker.get_allowed_types() - if not _resource_type_matches(_clip_resource, allowed): - var attendu := ", ".join(allowed) if not allowed.is_empty() else "?" - _flash(L.t("Type incompatible : ce champ attend « %s »") % attendu, true) - return false - # set_edited_resource() only refreshes the field's display: without emitting - # resource_changed, the Inspector never propagates the value to the object's real property - # (e.g. MeshInstance3D.mesh), which then overwrites the display on the next refresh. - picker.set_edited_resource(_clip_resource) - picker.emit_signal("resource_changed", _clip_resource) - _flash(L.t("« %s » collée dans l'Inspecteur") % _clip_resource_name) - return true - - -## Lightweight loop that makes the floating "Paste" button appear/follow above -## any compatible hovered resource field (only when a resource is copied). -func _update_paste_pop_hover() -> void: - if _clip_resource == null or not is_instance_valid(_clip_resource): - if _paste_pop.visible: - _hide_paste_pop() - return - - var vp := EditorInterface.get_base_control().get_viewport() - var hovered: Control = vp.gui_get_hovered_control() if vp else null - - # Do not hide while heading to the button to click it. - if hovered != null and (hovered == _paste_pop or _paste_pop.is_ancestor_of(hovered)): - return - - var picker := _picker_under(hovered) - if not _picker_accepts_clip(picker): - if _paste_pop.visible: - _hide_paste_pop() - return - - var is_new := picker != _paste_target - _paste_target = picker - _position_paste_pop(picker) - if is_new or not _paste_pop.visible: - _show_paste_pop() - - -func _position_paste_pop(picker: EditorResourcePicker) -> void: - var rect := picker.get_global_rect() - var origin: Vector2 = _toast_overlay.global_position - var btn_size := _paste_pop.size - - # Left side of the field, vertically centered: the right stays free for Godot's native - # quick menu and dropdown arrow. Always clamped inside the field. - var x: float = rect.position.x + 2.0 - x = minf(x, rect.position.x + rect.size.x - btn_size.x - 2.0) - x = maxf(x, rect.position.x + 1.0) - var y: float = rect.position.y + (rect.size.y - btn_size.y) * 0.5 - y = clampf(y, rect.position.y + 1.0, rect.position.y + maxf(rect.size.y - btn_size.y - 1.0, 1.0)) - var pos := Vector2(x, y) - origin - _paste_pop.position = pos - - -func _show_paste_pop() -> void: - _paste_hiding = false - if _paste_tween and _paste_tween.is_valid(): - _paste_tween.kill() - _paste_pop.visible = true - _paste_pop.pivot_offset = _paste_pop.size * 0.5 - _paste_pop.modulate.a = 0.0 - _paste_pop.scale = Vector2(0.8, 0.8) - _paste_tween = create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) - _paste_tween.tween_property(_paste_pop, "modulate:a", 1.0, 0.12) - _paste_tween.parallel().tween_property(_paste_pop, "scale", Vector2.ONE, 0.16) - - -func _hide_paste_pop() -> void: - _paste_target = null - if _paste_hiding or not is_instance_valid(_paste_pop): - return - if _paste_tween and _paste_tween.is_valid(): - _paste_tween.kill() - _paste_hiding = true - _paste_tween = create_tween().set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - _paste_tween.tween_property(_paste_pop, "modulate:a", 0.0, 0.08) - _paste_tween.tween_callback(func() -> void: - _paste_hiding = false - if is_instance_valid(_paste_pop): - _paste_pop.visible = false - ) - - -func _on_paste_pop_pressed() -> void: - if not _picker_accepts_clip(_paste_target): - _hide_paste_pop() - return - _paste_target.set_edited_resource(_clip_resource) - _paste_target.emit_signal("resource_changed", _clip_resource) - _flash(L.t("« %s » collée dans l'Inspecteur") % _clip_resource_name) - _hide_paste_pop() - - -## Lightweight loop that makes the floating "Copy" button appear/follow above -## the drawer's list resource currently hovered by the mouse. -func _update_copy_pop_hover() -> void: - var vp := EditorInterface.get_base_control().get_viewport() - var hovered: Control = vp.gui_get_hovered_control() if vp else null - - # Do not hide while heading to the button to click it. - if hovered != null and (hovered == _copy_pop or _copy_pop.is_ancestor_of(hovered)): - return - if hovered == null or not (hovered == _list or _list.is_ancestor_of(hovered)): - if _copy_pop.visible: - _hide_copy_pop() - return - - var idx := _list.get_item_at_position(_list.get_local_mouse_position(), true) - if idx < 0: - if _copy_pop.visible: - _hide_copy_pop() - return - var path := str(_list.get_item_metadata(idx)) - if path.is_empty() or DirAccess.dir_exists_absolute(path) or not ResourceLoader.exists(path): - if _copy_pop.visible: - _hide_copy_pop() - return - - if not _position_copy_pop(idx): - # item (almost) entirely out of the visible area: no button - if _copy_pop.visible: - _hide_copy_pop() - return - var is_new := path != _copy_target_path or not _copy_pop.visible - _copy_target_path = path - if is_new: - _show_copy_pop() - - -func _position_copy_pop(idx: int) -> bool: - # The button lives in an unclipped layer: we explicitly keep it inside the - # VISIBLE part of the list (a file half out at the top must not make it overflow onto - # the search bar / the path). Returns false if there is no visible room. - var item_rect := _item_rect(idx, false) - var vbar := _list.get_v_scroll_bar() - var view_w := _list.size.x - (vbar.size.x if vbar.visible else 0.0) - var view := Rect2(Vector2.ZERO, Vector2(view_w, _list.size.y)) - var vis := item_rect.intersection(view) - var btn_size := _copy_pop.size - if vis.size.x < 8.0 or vis.size.y < minf(btn_size.y * 0.6, 10.0): - return false - var origin: Vector2 = _toast_overlay.global_position - var list_origin: Vector2 = _list.global_position - - # Top-right corner of the visible part of the item - var x: float = vis.end.x - btn_size.x - 3.0 - x = maxf(x, vis.position.x + 1.0) - var y: float = vis.position.y + 3.0 - y = minf(y, vis.position.y + maxf(vis.size.y - btn_size.y - 1.0, 1.0)) - # Final safeguard: never outside the list - x = clampf(x, 0.0, maxf(view_w - btn_size.x, 0.0)) - y = clampf(y, 0.0, maxf(_list.size.y - btn_size.y, 0.0)) - _copy_pop.position = list_origin + Vector2(x, y) - origin - return true - - -func _show_copy_pop() -> void: - _copy_hiding = false - if _copy_tween and _copy_tween.is_valid(): - _copy_tween.kill() - _copy_pop.visible = true - _copy_pop.pivot_offset = _copy_pop.size * 0.5 - _copy_pop.modulate.a = 0.0 - _copy_pop.scale = Vector2(0.8, 0.8) - _copy_tween = create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) - _copy_tween.tween_property(_copy_pop, "modulate:a", 1.0, 0.12) - _copy_tween.parallel().tween_property(_copy_pop, "scale", Vector2.ONE, 0.16) - - -func _hide_copy_pop() -> void: - _copy_target_path = "" - # Already fading out: above all, do not restart the tween (the hover loop - # recreated it every frame, which froze the fade and made it very slow). - if _copy_hiding or not is_instance_valid(_copy_pop): - return - if _copy_tween and _copy_tween.is_valid(): - _copy_tween.kill() - _copy_hiding = true - _copy_tween = create_tween().set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - _copy_tween.tween_property(_copy_pop, "modulate:a", 0.0, 0.08) - _copy_tween.tween_callback(func() -> void: - _copy_hiding = false - if is_instance_valid(_copy_pop): - _copy_pop.visible = false - ) - - -func _on_copy_pop_pressed() -> void: - if _copy_target_path.is_empty(): - _hide_copy_pop() - return - _copy_resource_to_clipboard(_copy_target_path) - _bounce_to(_copy_pop, Vector2(1.2, 1.2), 0.08) - get_tree().create_timer(0.08).timeout.connect(func() -> void: - if is_instance_valid(_copy_pop): - _bounce_to(_copy_pop, Vector2.ONE, 0.14) - ) - - -func _place_ctx_popup() -> void: - _ctx_popup.visible = true - _ctx_popup.reset_size() - _ctx_popup.size = _ctx_popup.get_combined_minimum_size() - var ov: Vector2 = _toast_overlay.size - var pos: Vector2 = _toast_overlay.get_local_mouse_position() - var sz: Vector2 = _ctx_popup.size - pos.x = clampf(pos.x, 4.0, maxf(4.0, ov.x - sz.x - 4.0)) - pos.y = clampf(pos.y, 4.0, maxf(4.0, ov.y - sz.y - 4.0)) - _ctx_popup.position = pos - _ctx_popup.modulate.a = 0.0 - create_tween().tween_property(_ctx_popup, "modulate:a", 1.0, 0.08) - - -func _add_color_row(target: String) -> void: - var color_lbl := Label.new() - color_lbl.text = L.t("COULEUR DU DOSSIER") - color_lbl.modulate = TEXT_MUTED - color_lbl.add_theme_font_size_override("font_size", 10) - _ctx_vbox.add_child(color_lbl) - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 4) - for key in FOLDER_COLORS.keys(): - var cb := Button.new() - cb.custom_minimum_size = Vector2(16, 16) - cb.flat = false - cb.tooltip_text = str(key) - cb.add_theme_stylebox_override("normal", _make_stylebox(FOLDER_COLORS[key], Color.TRANSPARENT, 8)) - cb.add_theme_stylebox_override("hover", _make_stylebox(FOLDER_COLORS[key], Color.WHITE, 8, 2)) - _add_press_bounce(cb, 1.25, 0.85) - var ck: String = key - cb.pressed.connect(func() -> void: - _ctx_popup.hide() - _set_folder_color(target, ck) - ) - row.add_child(cb) - var rb := Button.new() - rb.custom_minimum_size = Vector2(16, 16) - rb.flat = false - rb.tooltip_text = L.t("Couleur par défaut") - rb.add_theme_stylebox_override("normal", _make_stylebox(Color(0.2, 0.2, 0.2), Color.GRAY, 8, 1)) - _add_press_bounce(rb, 1.25, 0.85) - rb.pressed.connect(func() -> void: - _ctx_popup.hide() - _set_folder_color(target, "") - ) - row.add_child(rb) - _ctx_vbox.add_child(row) - - -func _show_context() -> void: - for c in _ctx_vbox.get_children(): - _ctx_vbox.remove_child(c) - c.queue_free() - var sel := _selected_paths() - var on_item := not sel.is_empty() - var target := _context_path - var is_dir := DirAccess.dir_exists_absolute(target) - var single := sel.size() == 1 - - if on_item: - if single: - _ctx_btn(L.t("Ouvrir"), func() -> void: _open_path(target), Color.TRANSPARENT, "Load") - if not is_dir and _is_model(target): - _ctx_btn(L.t("Import avancé..."), func() -> void: _open_advanced_import(target), Color.TRANSPARENT, "Tools") - if _is_model_scene(target): - _ctx_btn(L.t("Nouvelle scène héritée"), func() -> void: _new_inherited_scene(target), Color.TRANSPARENT, "PackedScene") - _ctx_btn(L.t("Afficher dans le dock Fichiers"), func() -> void: _show_in_dock(target), Color.TRANSPARENT, "Filesystem") - _ctx_btn(L.t("Afficher dans l'explorateur"), func() -> void: - OS.shell_show_in_file_manager(ProjectSettings.globalize_path(target.trim_suffix("/"))) - , Color.TRANSPARENT, "ExternalLink") - if single and _active_set.is_empty() and not _search.text.strip_edges().is_empty(): - _ctx_btn(L.t("Afficher dans son dossier"), func() -> void: - _pending_select = target - _navigate(target) - , Color.TRANSPARENT, "Folder") - _ctx_sep() - _ctx_btn(L.t("Copier le chemin"), func() -> void: - DisplayServer.clipboard_set("\n".join(sel)) - _flash(L.t("Chemin copié")) - , Color.TRANSPARENT, "ActionCopy") - _ctx_btn(L.t("Copier le chemin absolu"), func() -> void: - var abs_paths: PackedStringArray = [] - for p in sel: - abs_paths.append(ProjectSettings.globalize_path(p)) - DisplayServer.clipboard_set("\n".join(abs_paths)) - _flash(L.t("Chemin absolu copié")) - , Color.TRANSPARENT, "ActionCopy") - if single and not is_dir: - _ctx_btn(L.t("Copier la ressource (bouton Coller au survol)"), func() -> void: - _copy_resource_to_clipboard(target) - , COLOR_RES_ACTION, "ActionCopy") - if single and not is_dir and ResourceLoader.has_method("get_resource_uid"): - _ctx_btn(L.t("Copier l'UID"), func() -> void: - var uid: int = ResourceLoader.call("get_resource_uid", target) - if uid != -1: - DisplayServer.clipboard_set(ResourceUID.id_to_text(uid)) - _flash(L.t("UID copié")) - , Color.TRANSPARENT, "ActionCopy") - _ctx_sep() - - if not on_item: - var in_set := not _active_set.is_empty() - var here := current_dir - if not in_set: - _ctx_btn(L.t("Ouvrir le dossier dans l'explorateur"), func() -> void: - OS.shell_show_in_file_manager(ProjectSettings.globalize_path(here)) - , Color.TRANSPARENT, "ExternalLink") - _ctx_btn(L.t("Afficher dans le dock Fichiers"), func() -> void: _show_in_dock(here), Color.TRANSPARENT, "Filesystem") - _ctx_btn(L.t("Copier le chemin du dossier"), func() -> void: - DisplayServer.clipboard_set(here) - _flash(L.t("Chemin copié")) - , Color.TRANSPARENT, "ActionCopy") - _ctx_btn(L.t("Copier le chemin absolu du dossier"), func() -> void: - DisplayServer.clipboard_set(ProjectSettings.globalize_path(here)) - _flash(L.t("Chemin absolu copié")) - , Color.TRANSPARENT, "ActionCopy") - if here != "res://": - _ctx_btn(L.t("Retirer le dossier des favoris") if _is_fav(here) else L.t("Ajouter le dossier aux favoris"), func() -> void: - var was_fav := _is_fav(here) - _toggle_favorite(here) - _refresh() - _flash(L.t("Retiré des favoris") if was_fav else L.t("Ajouté aux favoris")) - , Color.TRANSPARENT, "Favorites") - _ctx_sep() - _ctx_btn(L.t("Actualiser"), func() -> void: - _scan() - _refresh() - _flash(L.t("Actualisé")) - , Color.TRANSPARENT, "Reload") - _ctx_btn(L.t("Passer en vue grille") if _view_list else L.t("Passer en vue liste"), func() -> void: - var btn: Button = _btn_grid if _view_list else _btn_list - btn.button_pressed = true - btn.pressed.emit() - , Color.TRANSPARENT, "Grid" if _view_list else "FileList") - _ctx_check(L.t("Afficher les éléments masqués"), _show_hidden, _set_show_hidden) - _ctx_sep() - - _ctx_btn(L.t("Nouveau dossier"), _create_folder, Color.TRANSPARENT, "Folder") - _ctx_btn(L.t("Nouvelle scène"), _create_scene, Color.TRANSPARENT, "PackedScene") - _ctx_btn(L.t("Nouveau script"), _create_script, Color.TRANSPARENT, "Script") - - if not on_item and _active_set.is_empty() and current_dir != "res://": - _ctx_sep() - _add_color_row(current_dir) - - if on_item: - _ctx_sep() - if single: - _ctx_btn(L.t("Renommer (F2)"), func() -> void: _rename(target), Color.TRANSPARENT, "Rename") - _ctx_btn(L.t("Dupliquer (Ctrl+D)"), func() -> void: _duplicate(sel), Color.TRANSPARENT, "Duplicate") - _ctx_btn(L.t("Retirer des favoris") if _is_fav(target) else L.t("Ajouter aux favoris"), func() -> void: - var was_fav := _is_fav(target) - for p in sel: - _toggle_favorite(p) - _refresh() - _flash(L.t("Retiré des favoris") if was_fav else L.t("Ajouté aux favoris")) - , Color.TRANSPARENT, "Favorites") - _ctx_btn(L.t("Ajouter à un set..."), func() -> void: - var default_name := str(_sets.keys()[0]) if not _sets.is_empty() else L.t("mon_set") - _prompt_string(L.t("Ajouter au set"), default_name, func(n: String) -> void: - if not _valid_name(n): - return - var arr: Array = _sets.get(n, []) - for p in sel: - if not arr.has(p): - arr.append(p) - _sets[n] = arr - _save_cfg() - _refresh_left_tab_content() - _flash(L.t("Ajouté au set « %s »") % n) - ) - , Color.TRANSPARENT, "Groups") - if not _active_set.is_empty(): - _ctx_btn(L.t("Retirer de ce set"), func() -> void: - var arr: Array = _sets.get(_active_set, []) - for p in sel: - arr.erase(p) - _sets[_active_set] = arr - _save_cfg() - _refresh() - ) - var to_hide: PackedStringArray = [] - var to_unhide: PackedStringArray = [] - for hp in sel: - if _hidden.has(_hidden_key(hp)): - to_unhide.append(hp) - elif hp != "res://" and not _is_hidden(hp): - to_hide.append(hp) - if not to_hide.is_empty(): - _ctx_btn(L.t("Masquer dans le tiroir"), func() -> void: _hide_paths(to_hide, true), Color.TRANSPARENT, "GuiVisibilityHidden") - if not to_unhide.is_empty(): - _ctx_btn(L.t("Ne plus masquer"), func() -> void: _hide_paths(to_unhide, false), Color.TRANSPARENT, "GuiVisibilityVisible") - _ctx_check(L.t("Afficher les éléments masqués"), _show_hidden, _set_show_hidden) - if not is_dir: - _ctx_btn(L.t("Réimporter"), func() -> void: - var files: PackedStringArray = [] - for p in sel: - if not DirAccess.dir_exists_absolute(p): - files.append(p) - EditorInterface.get_resource_filesystem().reimport_files(files) - , Color.TRANSPARENT, "Reload") - if is_dir and single: - _ctx_sep() - _add_color_row(target) - _ctx_sep() - _ctx_btn(L.t("Supprimer (Suppr)"), func() -> void: _confirm_delete(sel), COLOR_DANGER, "Remove") - - _place_ctx_popup() - # PopupPanel is a Window (no "scale"/"modulate"): we animate its content instead. - _ctx_vbox.modulate.a = 0.0 - create_tween().tween_property(_ctx_vbox, "modulate:a", 1.0, 0.11) \ - .set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - - -# ---------- Creation / renaming / deletion ---------- - -func _valid_name(n: String) -> bool: - if n.strip_edges().is_empty(): - return false - for ch in ["/", "\\", ":", "*", "?", "\"", "<", ">", "|"]: - if n.contains(ch): - _flash(L.t("Nom invalide : caractère « %s » interdit") % ch, true) - return false - return true - - -func _unique_path(dir: String, base_name: String, ext: String) -> String: - var p := dir.path_join(base_name + ext) - var i := 2 - while FileAccess.file_exists(p) or DirAccess.dir_exists_absolute(p): - p = dir.path_join("%s_%d%s" % [base_name, i, ext]) - i += 1 - return p - - -func _scan() -> void: - EditorInterface.get_resource_filesystem().scan() - - -func _target_dir() -> String: - # If a folder is selected we create inside it, otherwise in the current folder - return current_dir - - -func _create_folder() -> void: - var dir := _target_dir() - _prompt_string(L.t("Nouveau dossier"), _unique_path(dir, L.t("nouveau_dossier"), "").get_file(), func(n: String) -> void: - if not _valid_name(n): - return - var p := dir.path_join(n) - if DirAccess.dir_exists_absolute(p) or FileAccess.file_exists(p): - _flash(L.t("« %s » existe déjà") % n, true) - return - DirAccess.make_dir_recursive_absolute(p) - _pending_select = p - _scan() - ) - - -func _create_scene() -> void: - var dir := _target_dir() - _prompt_string(L.t("Nouvelle scène"), _unique_path(dir, L.t("nouvelle_scene"), ".tscn").get_file(), func(n: String) -> void: - if not _valid_name(n): - return - var p := dir.path_join(n if n.ends_with(".tscn") else n + ".tscn") - if FileAccess.file_exists(p): - _flash(L.t("« %s » existe déjà") % n, true) - return - var node := Node2D.new() - node.name = p.get_file().get_basename().to_pascal_case() - var packed := PackedScene.new() - packed.pack(node) - ResourceSaver.save(packed, p) - node.free() - _pending_select = p - _scan() - ) - - -## True only if the project already has a C# solution (a .csproj file at the root). -## Without it, Godot does not index .cs scripts in its EditorFileSystem: the file -## would exist on disk but stay invisible everywhere in the editor, not only -## in the drawer (that is why we block creation rather than let a "ghost" -## file be created that would later make people think it already exists). -func _has_csharp_project() -> bool: - var d := DirAccess.open("res://") - if d == null: - return false - d.list_dir_begin() - var f := d.get_next() - while f != "": - if not d.current_is_dir() and f.get_extension().to_lower() == "csproj": - return true - f = d.get_next() - return false - - -## Languages offered in the dropdown: only those THIS Godot build knows -## about (ScriptServer only lists "C#" on the .NET edition, for example), exactly -## like Godot's native "Create Script" dialog does. We add "Shader" (not a -## ScriptLanguage in its own right) and "Custom..." (free extension) at the end. -## Languages offered in the dropdown: only those THIS Godot build knows -## about. `ScriptServer` (used internally by the native "Create Script" dialog) is not -## exposed to GDScript, so we detect differently: GDScript is always there, and C# only -## if this build has the "mono" feature tag (the .NET editions of Godot define it). -## Each entry is either {"sep": "Category name"} (a non-selectable separator in -## the menu), or {"name": ..., "ext": ...} (a real option). -func _script_languages() -> Array: - var out: Array = [] - out.append({"sep": L.t("Scripts")}) - out.append({"name": "GDScript", "ext": "gd"}) - if OS.has_feature("mono"): - out.append({"name": "C#", "ext": "cs"}) - out.append({"name": L.t("Shader"), "ext": "gdshader"}) - # Common text file types, not tied to a particular script language - # (creating a simple .txt or .json next to the assets is a frequent need). - out.append({"sep": L.t("Fichiers texte")}) - out.append({"name": L.t("Texte"), "ext": "txt"}) - out.append({"name": "JSON", "ext": "json"}) - out.append({"name": "Markdown", "ext": "md"}) - out.append({"name": L.t("Configuration"), "ext": "cfg"}) - out.append({"name": "XML", "ext": "xml"}) - out.append({"name": "CSV", "ext": "csv"}) - out.append({"sep": ""}) - out.append({"name": L.t("Personnalisé..."), "ext": ""}) - return out - - -func _create_script() -> void: - var dir := _target_dir() - var langs := _script_languages() - - var d := ConfirmationDialog.new() - d.title = L.t("Nouveau script") - d.ok_button_text = L.t("Créer") - d.cancel_button_text = L.t("Annuler") - - var vb := VBoxContainer.new() - vb.custom_minimum_size.x = 340 - vb.add_theme_constant_override("separation", 8) - d.add_child(vb) - - var lang_row := HBoxContainer.new() - lang_row.add_theme_constant_override("separation", 8) - vb.add_child(lang_row) - var lang_lbl := Label.new() - lang_lbl.text = L.t("Langage") - lang_lbl.custom_minimum_size.x = 70 - lang_row.add_child(lang_lbl) - var lang_opt := OptionButton.new() - lang_opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL - # Mapping between the menu index (category separators count as one - # slot, even though they cannot be selected) and the real index in "langs": -1 for - # a separator, otherwise the index of the matching entry in "langs". - var entry_of_index: Array = [] - for i in langs.size(): - var entry: Dictionary = langs[i] - if entry.has("sep"): - lang_opt.add_separator(entry.sep) - entry_of_index.append(-1) - else: - lang_opt.add_item(entry.name) - entry_of_index.append(i) - lang_row.add_child(lang_opt) - - var custom_row := HBoxContainer.new() - custom_row.add_theme_constant_override("separation", 8) - custom_row.visible = false - vb.add_child(custom_row) - var custom_lbl := Label.new() - custom_lbl.text = L.t("Extension") - custom_lbl.custom_minimum_size.x = 70 - custom_row.add_child(custom_lbl) - var custom_edit := LineEdit.new() - custom_edit.placeholder_text = "txt" - custom_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL - custom_row.add_child(custom_edit) - - var name_row := HBoxContainer.new() - name_row.add_theme_constant_override("separation", 8) - vb.add_child(name_row) - var name_lbl := Label.new() - name_lbl.text = L.t("Nom") - name_lbl.custom_minimum_size.x = 70 - name_row.add_child(name_lbl) - var name_edit := LineEdit.new() - name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL - name_row.add_child(name_edit) - d.register_text_enter(name_edit) - - # Preselects the last language used for this drawer, otherwise GDScript (first selectable - # item: we skip any category separator at the head of the list). - var start_idx := 0 - for oi in entry_of_index.size(): - var li: int = entry_of_index[oi] - if li >= 0: - start_idx = oi - if langs[li].ext == _last_script_ext: - break - lang_opt.select(start_idx) - - # The suggested name updates with the chosen language's extension, like the - # "Path" field of Godot's native dialog when the language changes. - var update_name := func() -> void: - var entry: Dictionary = langs[entry_of_index[lang_opt.selected]] - var is_custom: bool = entry.ext.is_empty() - custom_row.visible = is_custom - var ext: String = custom_edit.text.strip_edges().trim_prefix(".") if is_custom else entry.ext - var suffix := ("." + ext) if not ext.is_empty() else "" - name_edit.text = _unique_path(dir, L.t("nouveau_script"), suffix).get_file().get_basename() - lang_opt.item_selected.connect(func(_i: int) -> void: update_name.call()) - custom_edit.text_changed.connect(func(_t: String) -> void: update_name.call()) - update_name.call() - - d.confirmed.connect(func() -> void: - var entry: Dictionary = langs[entry_of_index[lang_opt.selected]] - var is_custom: bool = entry.ext.is_empty() - var ext: String = (custom_edit.text.strip_edges().trim_prefix(".").to_lower()) if is_custom else entry.ext - var base_name := name_edit.text.strip_edges() - if base_name.is_empty() or ext.is_empty() or not _valid_name(base_name + "." + ext): - return - if ext == "cs" and not _has_csharp_project(): - _flash(L.t("Pas de solution C# dans ce projet : créez un premier script C# depuis le dock Fichiers de Godot (il générera le .csproj), puis réessayez ici"), true) - return - var fname := base_name + "." + ext - var p := dir.path_join(fname) - if FileAccess.file_exists(p) or DirAccess.dir_exists_absolute(p): - _flash(L.t("« %s » existe déjà") % fname, true) - return - var f := FileAccess.open(p, FileAccess.WRITE) - if f == null: - _flash(L.t("Impossible de créer « %s »") % fname, true) - return - f.store_string(_script_template(ext, base_name)) - f.close() - _last_script_ext = ext - _save_cfg() - _pending_select = p - _scan() - if ext == "cs" and EditorInterface.get_resource_filesystem().has_method("scan_sources"): - EditorInterface.get_resource_filesystem().call("scan_sources") - ) - d.visibility_changed.connect(func() -> void: - if not d.visible: - d.queue_free() - ) - EditorInterface.get_base_control().add_child(d) - d.popup_centered(Vector2i(380, 170)) - name_edit.grab_focus() - name_edit.select_all() - - -## Initial content depending on the extension: GDScript, C# and shader have a template, the rest is empty. -func _script_template(ext: String, base_name: String) -> String: - match ext: - "gd": - return "extends Node\n\n\nfunc _ready() -> void:\n\tpass\n" - "cs": - var cls := "" - for ch in base_name.to_pascal_case(): - if (ch >= "a" and ch <= "z") or (ch >= "A" and ch <= "Z") or (ch >= "0" and ch <= "9") or ch == "_": - cls += ch - if cls.is_empty() or (cls[0] >= "0" and cls[0] <= "9"): - cls = "_" + cls - return "using Godot;\n\npublic partial class %s : Node\n{\n\tpublic override void _Ready()\n\t{\n\t}\n}\n" % cls - "gdshader": - return "shader_type canvas_item;\n\nvoid fragment() {\n}\n" - "json": - return "{}\n" - "xml": - return "\n" - "md": - return "# %s\n" % base_name - return "" - - -func _rename(path: String) -> void: - var src := path.trim_suffix("/") - _prompt_string(L.t("Renommer"), src.get_file(), func(n: String) -> void: - if not _valid_name(n): - return - var dst := src.get_base_dir().path_join(n) - if dst == src: - return - if FileAccess.file_exists(dst) or DirAccess.dir_exists_absolute(dst): - _flash(L.t("« %s » existe déjà") % n, true) - return - EditorInterface.save_all_scenes() # here rather than before the dialog - var is_dir := DirAccess.dir_exists_absolute(src) # BEFORE the rename - if DirAccess.rename_absolute(src, dst) != OK: - _flash(L.t("Échec du renommage"), true) - return - for ext in [".import", ".uid"]: - if FileAccess.file_exists(src + ext): - DirAccess.rename_absolute(src + ext, dst + ext) - _rewrite_paths(src, dst) - current_dir = _swap_prefix(current_dir, src, dst) - for i in _history.size(): - _history[i] = _swap_prefix(_history[i], src, dst) - _update_references([[src, dst, is_dir]]) # ADDED - _pending_select = dst - _scan() - _flash(L.t("Renommé en « %s »") % n) - ) - -func _duplicate(paths: PackedStringArray) -> void: - var last := "" - for p in paths: - var src := p.trim_suffix("/") - var dir := src.get_base_dir() - if DirAccess.dir_exists_absolute(src): - var dst := _unique_path(dir, src.get_file() + L.t("_copie"), "") - _copy_dir(src, dst) - last = dst - else: - var dst := _unique_path(dir, src.get_file().get_basename() + L.t("_copie"), "." + src.get_extension()) - _copy_file(src, dst) - last = dst - _pending_select = last - _scan() - _flash(L.t("%d élément(s) dupliqué(s)") % paths.size() if paths.size() > 1 else L.t("Élément dupliqué")) - - -func _copy_dir(src: String, dst: String) -> void: - DirAccess.make_dir_recursive_absolute(dst) - for f in DirAccess.get_files_at(src): - if f.get_extension() == "import" or f.get_extension() == "uid": - continue - _copy_file(src.path_join(f), dst.path_join(f)) - for d in DirAccess.get_directories_at(src): - _copy_dir(src.path_join(d), dst.path_join(d)) - - -func _copy_file(src: String, dst: String) -> void: - DirAccess.copy_absolute(src, dst) - # A .tscn/.tres embeds its UID in the header: we remove it from the copy to - # avoid a duplicate UID (Godot assigns a new one). - if dst.get_extension().to_lower() in ["tscn", "tres"]: - var text := FileAccess.get_file_as_string(dst) - var nl := text.find("\n") - var head := text.substr(0, nl) if nl >= 0 else text - var a := head.find(" uid=\"") - if a >= 0: - var b := head.find("\"", a + 6) - if b > a: - var f := FileAccess.open(dst, FileAccess.WRITE) - if f: - f.store_string(head.substr(0, a) + head.substr(b + 1) + text.substr(head.length())) - f.close() - - -func _confirm_delete(paths: PackedStringArray) -> void: - var names: PackedStringArray = [] - for p in paths: - names.append(p.trim_suffix("/").get_file()) - var txt := L.t("Envoyer à la corbeille :\n") + "\n".join(names.slice(0, 8)) - if names.size() > 8: - txt += "\n... (+%d)" % (names.size() - 8) - _confirm(L.t("Supprimer"), txt, func() -> void: - var ok_count := 0 - for p in paths: - var src := p.trim_suffix("/") - var err := OS.move_to_trash(ProjectSettings.globalize_path(src)) - if err != OK: - _flash(L.t("Impossible de mettre « %s » à la corbeille") % src.get_file(), true) - continue - for ext in [".import", ".uid"]: - if FileAccess.file_exists(src + ext): - OS.move_to_trash(ProjectSettings.globalize_path(src + ext)) - _forget_path(src) - ok_count += 1 - _save_cfg() - _scan() - if ok_count > 0: - _flash(L.t("%d élément(s) envoyé(s) à la corbeille") % ok_count if ok_count > 1 else L.t("Envoyé à la corbeille")) - ) - - -func _forget_path(src: String) -> void: - var fav := Array(_favorites) - var rec := Array(_recents) - fav = fav.filter(func(p) -> bool: return not _is_under(str(p), src)) - rec = rec.filter(func(p) -> bool: return not _is_under(str(p), src)) - _favorites = PackedStringArray(fav) - _push_favorites() - _recents = PackedStringArray(rec) - _hidden = PackedStringArray(Array(_hidden).filter(func(p) -> bool: return not _is_under(str(p), src))) - for k in _sets.keys(): - var arr: Array = _sets[k] - _sets[k] = arr.filter(func(p) -> bool: return not _is_under(str(p), src)) - - -func _is_under(p: String, src: String) -> bool: - var sp := p.trim_suffix("/") - return sp == src or sp.begins_with(src + "/") - -const REF_EXTS := ["tscn", "tres", "gd", "gdshader", "gdshaderinc", "godot", "cfg"] - -func _collect_text_files(dir: String, out: Array) -> void: - for d in DirAccess.get_directories_at(dir): - if d.begins_with(".") or d == "addons": - continue - _collect_text_files(dir.path_join(d), out) - for f in DirAccess.get_files_at(dir): - if f.get_extension().to_lower() in REF_EXTS: - out.append(dir.path_join(f)) - - -# moves: Array of [src, dst, is_dir] -func _update_references(moves: Array) -> void: - if moves.is_empty(): - return - var files: Array = [] - _collect_text_files("res://", files) - var changed_files := 0 - for path in files: - var f := FileAccess.open(path, FileAccess.READ) - if f == null: - continue - var text := f.get_as_text() - f.close() - var new_text := text - for m in moves: - var src: String = m[0] - var dst: String = m[1] - # Exact path in quotes (avoids touching "x.tres2", etc.) - new_text = new_text.replace('"' + src + '"', '"' + dst + '"') - if m[2]: # folder: everything that starts with src + "/" - new_text = new_text.replace('"' + src + "/", '"' + dst + "/") - if new_text != text: - var w := FileAccess.open(path, FileAccess.WRITE) - if w: - w.store_string(new_text) - w.close() - changed_files += 1 - if changed_files > 0: - # Reload the open scenes, otherwise the editor may rewrite the old paths - for scene in EditorInterface.get_open_scenes(): - EditorInterface.reload_scene_from_path(scene) - -func _swap_prefix(p: String, src: String, dst: String) -> String: - var sp := p.trim_suffix("/") - var slash := "/" if p.ends_with("/") else "" - if sp == src: - return dst + slash - if sp.begins_with(src + "/"): - return dst + p.substr(src.length()) - return p - - -func _rewrite_paths(src: String, dst: String) -> void: - for i in _favorites.size(): - _favorites[i] = _swap_prefix(_favorites[i], src, dst) - _push_favorites() - for i in _recents.size(): - _recents[i] = _swap_prefix(_recents[i], src, dst) - for i in _hidden.size(): - _hidden[i] = _swap_prefix(_hidden[i], src, dst) - _tree_dirty = true - for k in _sets.keys(): - var arr: Array = _sets[k] - for i in arr.size(): - arr[i] = _swap_prefix(str(arr[i]), src, dst) - var colors: Dictionary = _colors() - var nc := {} - var changed := false - for k in colors.keys(): - var nk := _swap_prefix(str(k), src, dst) - changed = changed or nk != str(k) - nc[nk] = colors[k] - if changed: - ProjectSettings.set_setting("file_customization/folder_colors", nc) - ProjectSettings.save() - _save_cfg() - - -# ---------- Dialogs ---------- - -func _prompt_string(title: String, val: String, on_ok: Callable) -> void: - var d := ConfirmationDialog.new() - d.title = title - d.ok_button_text = L.t("Valider") - d.cancel_button_text = L.t("Annuler") - var le := LineEdit.new() - le.text = val - le.custom_minimum_size.x = 320 - d.add_child(le) - d.register_text_enter(le) - d.confirmed.connect(func() -> void: on_ok.call(le.text.strip_edges())) - d.visibility_changed.connect(func() -> void: - if not d.visible: - d.queue_free() - ) - EditorInterface.get_base_control().add_child(d) - d.popup_centered(Vector2i(360, 100)) - le.grab_focus() - le.select(0, val.get_basename().length() if val.contains(".") else val.length()) - - -func _confirm(title: String, text: String, on_ok: Callable) -> void: - var d := ConfirmationDialog.new() - d.title = title - d.dialog_text = text - d.ok_button_text = L.t("Confirmer") - d.cancel_button_text = L.t("Annuler") - d.confirmed.connect(func() -> void: on_ok.call()) - d.visibility_changed.connect(func() -> void: - if not d.visible: - d.queue_free() - ) - EditorInterface.get_base_control().add_child(d) - d.popup_centered() - - -# ---------- Favorites / recents / config ---------- - -# Godot's FileSystem dock favorites are authoritative (EditorSettings.get_favorites / set_favorites). -func _sync_favorites() -> void: - var es := EditorInterface.get_editor_settings() - if es == null or not es.has_method("get_favorites"): - return - var godot_favs := PackedStringArray() - for p in es.get_favorites(): - var n := str(p) - n = "res:/" if n == "res://" else n.trim_suffix("/") - if not godot_favs.has(n): - godot_favs.append(n) - if not _fav_migrated: - # Once only: favorites created before this version are added to Godot's - _fav_migrated = true - var added := false - for p in _favorites: - if not godot_favs.has(p) and (FileAccess.file_exists(p) or DirAccess.dir_exists_absolute(p)): - godot_favs.append(p) - added = true - _favorites = godot_favs - if added: - _push_favorites() - _save_cfg() - return - _favorites = godot_favs - - -func _push_favorites() -> void: - var es := EditorInterface.get_editor_settings() - if es == null or not es.has_method("set_favorites"): - return - var out := PackedStringArray() - for p in _favorites: - if p == "res:/": - out.append("res://") - elif DirAccess.dir_exists_absolute(p): - out.append(p + "/") - else: - out.append(p) - es.set_favorites(out) - - -func _is_fav(path: String) -> bool: - return _favorites.has(path.trim_suffix("/")) - - -func _toggle_favorite(path: String) -> void: - _sync_favorites() - var p := path.trim_suffix("/") - if _favorites.has(p): - var arr := Array(_favorites) - arr.erase(p) - _favorites = PackedStringArray(arr) - else: - _favorites.append(p) - _push_favorites() - _save_cfg() - - -func _push_recent(path: String) -> void: - var arr := Array(_recents) - arr.erase(path) - arr.push_front(path) - if arr.size() > 20: - arr.resize(20) - _recents = PackedStringArray(arr) - _save_cfg() - - -func _load_cfg() -> void: - var c := ConfigFile.new() - if c.load(CFG_PATH) == OK: - current_dir = c.get_value("state", "current_dir", "res://") - if int(c.get_value("state", "cfg_version", 1)) >= CFG_VERSION: - _height_ratio = c.get_value("state", "height_ratio", HEIGHT_RATIO) - _fav_migrated = bool(c.get_value("state", "fav_migrated", false)) - pinned = c.get_value("state", "pinned", false) - _view_list = c.get_value("state", "view_list", false) - _details_visible = c.get_value("state", "details_visible", true) - _favorites = c.get_value("state", "favorites", PackedStringArray()) - _recents = c.get_value("state", "recents", PackedStringArray()) - _sets = c.get_value("state", "sets", {}) - _zoom_grid = float(c.get_value("state", "zoom", 80.0)) - _zoom_list = float(c.get_value("state", "zoom_list", 22.0)) - _details_width = maxf(float(c.get_value("state", "details_width", 190.0)), DETAILS_MIN_WIDTH) - _hidden = c.get_value("state", "hidden", PackedStringArray()) - _show_hidden = bool(c.get_value("state", "show_hidden", false)) - _search_all = bool(c.get_value("state", "search_all", true)) - _last_script_ext = str(c.get_value("state", "script_ext", "gd")) - _history = PackedStringArray([current_dir]) - - -func _save_cfg() -> void: - # Deferred write: avoids writing the file at every zoom step / navigation - _save_dirty = true - if _save_timer != null and _save_timer.is_inside_tree(): - _save_timer.start() - - -func _flush_cfg() -> void: - if not _save_dirty: - return - _save_dirty = false - var c := ConfigFile.new() - c.set_value("state", "current_dir", current_dir) - c.set_value("state", "cfg_version", CFG_VERSION) - c.set_value("state", "fav_migrated", _fav_migrated) - c.set_value("state", "height_ratio", _height_ratio) - c.set_value("state", "pinned", pinned) - c.set_value("state", "view_list", _view_list) - c.set_value("state", "details_visible", _details_visible) - c.set_value("state", "favorites", _favorites) - c.set_value("state", "recents", _recents) - c.set_value("state", "sets", _sets) - c.set_value("state", "zoom", _zoom_grid) - c.set_value("state", "zoom_list", _zoom_list) - c.set_value("state", "details_width", _details_width) - c.set_value("state", "hidden", _hidden) - c.set_value("state", "show_hidden", _show_hidden) - c.set_value("state", "search_all", _search_all) - c.set_value("state", "script_ext", _last_script_ext) - c.save(CFG_PATH) diff --git a/addons/asset_drawer/drawer.gd.uid b/addons/asset_drawer/drawer.gd.uid deleted file mode 100644 index bdd483a..0000000 --- a/addons/asset_drawer/drawer.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ci1u46akkd1c diff --git a/addons/asset_drawer/lang.gd b/addons/asset_drawer/lang.gd deleted file mode 100644 index e80ea91..0000000 --- a/addons/asset_drawer/lang.gd +++ /dev/null @@ -1,201 +0,0 @@ -@tool -extends RefCounted -## Asset Drawer localization. -## The texts in the code are written in French (the key). If the Godot editor language is -## French, they are displayed as is; otherwise the English translation below is used -## (and, failing a translation, the original text). The language is read once per session: -## changing the editor language requires restarting it anyway. - - -const EN := { - "Scripts": "Scripts", - "Shader": "Shader", - "Fichiers texte": "Text files", - "Texte": "Text", - "Configuration": "Configuration", - "Personnalisé...": "Custom...", - "Créer": "Create", - "Langage": "Language", - "Extension": "Extension", - "Nom": "Name", - "Pas de solution C# dans ce projet : créez un premier script C# depuis le dock Fichiers de Godot (il générera le .csproj), puis réessayez ici": "No C# solution in this project: create a first C# script from Godot's FileSystem dock (it will generate the .csproj), then try again here", - "Déplacer ici": "Move here", - "Import avancé...": "Advanced import...", - "Import avancé indisponible pour ce fichier": "Advanced import unavailable for this file", - "Paramètres d'import": "Import settings", - "Nouvelle scène héritée": "New inherited scene", - "Échec du déplacement": "Move failed", - "%d élément(s) déplacé(s)": "%d item(s) moved", - "Élément déplacé": "Item moved", - "Dossiers": "Folders", - "Favoris": "Favorites", - "Récents": "Recents", - "Tout replier": "Collapse all", - "Localiser le dossier courant": "Locate current folder", - "+ Nouveau set": "+ New set", - "Nouveau set": "New set", - "mon_set": "my_set", - "Aucun asset": "No assets", - "Créer un dossier": "Create a folder", - "Réinitialiser les filtres": "Reset filters", - "Coller": "Paste", - "Coller la ressource copiée ici": "Paste the copied resource here", - "Copier": "Copy", - "Copier la ressource": "Copy resource", - "Ctrl+Espace": "Ctrl+Space", - "Précédent (bouton souris 4)": "Back (mouse button 4)", - "Suivant (bouton souris 5)": "Forward (mouse button 5)", - "Dossier parent (Retour arrière)": "Parent folder (Backspace)", - "Rechercher... (Ctrl+F)": "Search... (Ctrl+F)", - "Grille": "Grid", - "Vue grille": "Grid view", - "Liste": "List", - "Vue liste": "List view", - "Taille des miniatures (Ctrl+molette)": "Thumbnail size (Ctrl+wheel)", - "Détails": "Details", - "Panneau de détails": "Details panel", - "Épingler": "Pin", - "Épingler (reste ouvert)": "Pin (stays open)", - "Afficher ce dossier dans le dock Fichiers": "Show this folder in the FileSystem dock", - "Fermer (Échap)": "Close (Esc)", - "Tout": "All", - "Scènes": "Scenes", - "Modèles": "Models", - "Taille": "Size", - "Modifié": "Modified", - "Chemin": "Path", - "Ouvrir": "Open", - "Puis survolez un champ ressource de l'Inspecteur (ex. Mesh d'un MeshInstance3D) : un bouton « Coller » apparaît (ou Alt+V au clavier)": "Then hover a resource field in the Inspector (e.g. the Mesh of a MeshInstance3D): a “Paste” button appears (or press Alt+V)", - "Coller la ressource": "Paste resource", - "Copier le chemin": "Copy path", - "Chemin copié": "Path copied", - "Ajouter aux favoris": "Add to favorites", - "Retiré des favoris": "Removed from favorites", - "Ajouté aux favoris": "Added to favorites", - "+Dossier": "+Folder", - "Nouveau dossier": "New folder", - "+Scène": "+Scene", - "Clic droit : supprimer le set": "Right-click: delete set", - "\nClic droit : retirer": "\nRight-click: remove", - "Supprimer le set": "Delete set", - "Supprimer le set « %s » ? (les fichiers ne sont pas touchés)": "Delete set “%s”? (files are not touched)", - "Aucun résultat avec ce filtre": "No results with this filter", - "Set vide": "Empty set", - "Aucun résultat": "No results", - "Taille des icônes en liste (Ctrl+molette)": "Icon size in list view (Ctrl+wheel)", - "Taille des miniatures en grille (Ctrl+molette)": "Thumbnail size in grid view (Ctrl+wheel)", - "%d éléments sélectionnés": "%d items selected", - "Set : ": "Set: ", - "%d élément%s": "%d item%s", - " (limité à %d)": " (limited to %d)", - "à l'instant": "just now", - "il y a %d min": "%d min ago", - "il y a %d h": "%d h ago", - "hier": "yesterday", - "Dossier": "Folder", - "%d o": "%d B", - "%.1f Ko": "%.1f KB", - "%.1f Mo": "%.1f MB", - "Retirer des favoris": "Remove from favorites", - "Cliquer pour ouvrir le tiroir d'assets": "Click to open the asset drawer", - "Impossible de copier un dossier comme ressource": "Can't copy a folder as a resource", - "Ce fichier n'est pas une ressource Godot": "This file is not a Godot resource", - "Impossible de charger la ressource": "Can't load the resource", - "« %s » copiée — survolez un champ ressource de l'Inspecteur": "“%s” copied — hover a resource field in the Inspector", - "Aucune ressource copiée : clic droit sur un fichier → « Copier la ressource »": "No resource copied: right-click a file → “Copy resource”", - "Survolez un champ ressource de l'Inspecteur puis Alt+V": "Hover a resource field in the Inspector, then press Alt+V", - "Ce champ n'est pas modifiable": "This field is read-only", - "Type incompatible : ce champ attend « %s »": "Incompatible type: this field expects “%s”", - "« %s » collée dans l'Inspecteur": "“%s” pasted into the Inspector", - "COULEUR DU DOSSIER": "FOLDER COLOR", - "Couleur par défaut": "Default color", - "Afficher dans le dock Fichiers": "Show in FileSystem dock", - "Afficher dans l'explorateur": "Show in file manager", - "Copier le chemin absolu": "Copy absolute path", - "Chemin absolu copié": "Absolute path copied", - "Copier la ressource (bouton Coller au survol)": "Copy resource (Paste button on hover)", - "Copier l'UID": "Copy UID", - "UID copié": "UID copied", - "Ouvrir le dossier dans l'explorateur": "Open folder in file manager", - "Copier le chemin du dossier": "Copy folder path", - "Copier le chemin absolu du dossier": "Copy folder absolute path", - "Retirer le dossier des favoris": "Remove folder from favorites", - "Ajouter le dossier aux favoris": "Add folder to favorites", - "Actualiser": "Refresh", - "Actualisé": "Refreshed", - "Passer en vue grille": "Switch to grid view", - "Passer en vue liste": "Switch to list view", - "Nouvelle scène": "New scene", - "Nouveau script": "New script", - "Renommer (F2)": "Rename (F2)", - "Dupliquer (Ctrl+D)": "Duplicate (Ctrl+D)", - "Ajouter à un set...": "Add to a set...", - "Ajouter au set": "Add to set", - "Ajouté au set « %s »": "Added to set “%s”", - "Retirer de ce set": "Remove from this set", - "Réimporter": "Reimport", - "Supprimer (Suppr)": "Delete (Del)", - "Nom invalide : caractère « %s » interdit": "Invalid name: character “%s” not allowed", - "nouveau_dossier": "new_folder", - "« %s » existe déjà": "“%s” already exists", - "nouvelle_scene": "new_scene", - "nouveau_script": "new_script", - "Renommer": "Rename", - "Échec du renommage": "Rename failed", - "Renommé en « %s »": "Renamed to “%s”", - "_copie": "_copy", - "%d élément(s) dupliqué(s)": "%d item(s) duplicated", - "Élément dupliqué": "Item duplicated", - "Envoyer à la corbeille :\n": "Move to trash:\n", - "Supprimer": "Delete", - "Impossible de mettre « %s » à la corbeille": "Can't move “%s” to trash", - "%d élément(s) envoyé(s) à la corbeille": "%d item(s) moved to trash", - "Envoyé à la corbeille": "Moved to trash", - "Valider": "OK", - "Annuler": "Cancel", - "Confirmer": "Confirm", - " · %d sélectionné%s": " · %d selected%s", - "Asset Drawer (Ctrl+Espace)": "Asset Drawer (Ctrl+Space)", - "Projet": "Project", - "Rechercher dans tout le projet... (Ctrl+F)": "Search the whole project... (Ctrl+F)", - "Rechercher dans ce dossier... (Ctrl+F)": "Search this folder... (Ctrl+F)", - "Recherche : tout le projet (cliquer pour limiter au dossier courant)": "Search: whole project (click to limit to the current folder)", - "Recherche : dossier courant (cliquer pour chercher dans tout le projet)": "Search: current folder (click to search the whole project)", - "Masqué dans le tiroir": "Hidden in the drawer", - "Masqué dans le tiroir · clic droit → « Afficher les éléments masqués »": "Hidden in the drawer · right-click → “Show hidden items”", - "Réaffiché": "Unhidden", - "Afficher les éléments masqués": "Show hidden items", - "Masquer dans le tiroir": "Hide in the drawer", - "Ne plus masquer": "Unhide", - "Afficher dans son dossier": "Show in its folder", - "Impossible de créer « %s »": "Can't create “%s”", -} - -static var _fr := -1 - - -static func is_fr() -> bool: - if _fr < 0: - _fr = 1 if _detect_fr() else 0 - return _fr == 1 - - -static func t(fr: String) -> String: - if is_fr(): - return fr - return str(EN.get(fr, fr)) - - -## Marque du pluriel "s" (français uniquement) pour les mots comme "sélectionné(s)". -static func sel_s(plural: bool) -> String: - return "s" if plural and is_fr() else "" - - -static func _detect_fr() -> bool: - var loc := "" - if TranslationServer.has_method("get_tool_locale"): - loc = str(TranslationServer.call("get_tool_locale")) - else: - var lang := str(EditorInterface.get_editor_settings().get_setting("interface/editor/editor_language")) - loc = OS.get_locale() if lang == "" or lang == "auto" else lang - return loc.to_lower().begins_with("fr") diff --git a/addons/asset_drawer/lang.gd.uid b/addons/asset_drawer/lang.gd.uid deleted file mode 100644 index 1dcff4b..0000000 --- a/addons/asset_drawer/lang.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://0e4w5ydeh6np diff --git a/addons/asset_drawer/plugin.cfg b/addons/asset_drawer/plugin.cfg deleted file mode 100644 index 050a43c..0000000 --- a/addons/asset_drawer/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Asset Drawer" -description="Tiroir d'assets façon Content Drawer d'Unreal Engine. Ouvrir avec Ctrl+Espace." -author="Asset Drawer" -version="3.7" -script="plugin.gd" \ No newline at end of file diff --git a/addons/asset_drawer/plugin.gd b/addons/asset_drawer/plugin.gd deleted file mode 100644 index ef3e015..0000000 --- a/addons/asset_drawer/plugin.gd +++ /dev/null @@ -1,128 +0,0 @@ -@tool -extends EditorPlugin - -const Drawer := preload("res://addons/asset_drawer/drawer.gd") -const L := preload("res://addons/asset_drawer/lang.gd") - -# true : Ctrl+Space opens the drawer even when the script editor has focus -# (Ctrl+Space autocompletion is then replaced by the drawer). -# false : in the script editor, Ctrl+Space stays autocompletion. -const OPEN_IN_CODE_EDITOR := true - -# true : the open shortcut is ignored while the camera is in free look -# (right mouse button held in the 3D view, or free look toggled with Shift+F). -# This avoids opening the drawer when Ctrl (move down) + Space (move up) -# are pressed together to move around. -const BLOCK_IN_FREELOOK := true - -var drawer: Control -var _toolbar_btn: Button -var _theme_check_queued := false - -func _enter_tree() -> void: - _create_drawer() - - _toolbar_btn = Button.new() - _toolbar_btn.flat = true - _toolbar_btn.tooltip_text = L.t("Asset Drawer (Ctrl+Espace)") - _toolbar_btn.focus_mode = Control.FOCUS_NONE - var base := EditorInterface.get_base_control() - var icon_name := "FolderBrowse" if base.has_theme_icon("FolderBrowse", "EditorIcons") else "Folder" - _toolbar_btn.icon = base.get_theme_icon(icon_name, "EditorIcons") - _toolbar_btn.pressed.connect(func() -> void: - if is_instance_valid(drawer): - drawer.toggle()) - add_control_to_container(CONTAINER_TOOLBAR, _toolbar_btn) - - add_tool_menu_item("Asset Drawer", Callable(self, "_on_menu_toggle")) - set_process_input(true) - - # The drawer follows the editor theme (base color, contrast, accent). - var es := EditorInterface.get_editor_settings() - if not es.settings_changed.is_connected(_on_settings_changed): - es.settings_changed.connect(_on_settings_changed) - - -func _create_drawer() -> void: - drawer = Drawer.new() - drawer.name = "AssetDrawer" - EditorInterface.get_base_control().add_child(drawer) - - -func _on_settings_changed() -> void: - # Deferred: we first let the editor regenerate its theme before re-reading the colors. - if _theme_check_queued: - return - _theme_check_queued = true - _check_theme.call_deferred() - - -func _check_theme() -> void: - _theme_check_queued = false - if not is_instance_valid(drawer) or not drawer.palette_changed(): - return - # Recreates the drawer with the new palette. Its state (folder, height, favorites...) - # is already in the config, and is read back by the new drawer. - var was_open: bool = drawer.is_open - var old := drawer - drawer = null - old.get_parent().remove_child(old) # triggers _exit_tree: save + cleanup - old.queue_free() - await get_tree().process_frame # lets the old pill / overlay go away (same names) - if not is_inside_tree(): - return - _create_drawer() - if was_open: - drawer.open.call_deferred() - - -func _exit_tree() -> void: - var es := EditorInterface.get_editor_settings() - if is_instance_valid(es) and es.settings_changed.is_connected(_on_settings_changed): - es.settings_changed.disconnect(_on_settings_changed) - remove_tool_menu_item("Asset Drawer") - if is_instance_valid(_toolbar_btn): - remove_control_from_container(CONTAINER_TOOLBAR, _toolbar_btn) - _toolbar_btn.queue_free() - _toolbar_btn = null - if is_instance_valid(drawer): - drawer.queue_free() - drawer = null - - -func _on_menu_toggle() -> void: - if is_instance_valid(drawer): - drawer.toggle() - - -func _is_freelook_active() -> bool: - # During free look, Godot captures the mouse (right button held OR free look - # toggled with Shift+F). The right-click test acts as a safety net. - return Input.mouse_mode == Input.MOUSE_MODE_CAPTURED \ - or Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) - - -func _input(event: InputEvent) -> void: - if not is_instance_valid(drawer): - return - if not (event is InputEventKey and event.pressed and not event.echo): - return - var key := event as InputEventKey - if key.keycode == KEY_SPACE and key.ctrl_pressed and not key.alt_pressed and not key.shift_pressed and not key.meta_pressed: - # Free look active: we let the key through to the editor (camera movement). - if BLOCK_IN_FREELOOK and _is_freelook_active(): - return - # Ctrl+Space is also used for autocompletion: see OPEN_IN_CODE_EDITOR at the top of the file. - if not OPEN_IN_CODE_EDITOR: - var focus := EditorInterface.get_base_control().get_viewport().gui_get_focus_owner() - if focus is TextEdit and not drawer.is_ancestor_of(focus): - return - drawer.toggle() - get_viewport().set_input_as_handled() - elif key.keycode == KEY_ESCAPE and drawer.is_open and not drawer.pinned: - drawer.close() - get_viewport().set_input_as_handled() - elif key.keycode == KEY_V and key.alt_pressed and not key.ctrl_pressed and not key.shift_pressed and not key.meta_pressed: - # Pastes the copied resource from the drawer onto the Inspector resource field hovered by the mouse. - if drawer.try_paste_resource_at_mouse(): - get_viewport().set_input_as_handled() diff --git a/addons/asset_drawer/plugin.gd.uid b/addons/asset_drawer/plugin.gd.uid deleted file mode 100644 index 6a89b06..0000000 --- a/addons/asset_drawer/plugin.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bbthaayqxoorj diff --git a/assets/FreeLowPolyHouseInterior_NoAlpha/General/Floor/floor1.glb.import b/assets/FreeLowPolyHouseInterior_NoAlpha/General/Floor/floor1.glb.import index 753d4f9..4e7a859 100644 --- a/assets/FreeLowPolyHouseInterior_NoAlpha/General/Floor/floor1.glb.import +++ b/assets/FreeLowPolyHouseInterior_NoAlpha/General/Floor/floor1.glb.import @@ -39,7 +39,15 @@ import_script/path="" materials/extract=0 materials/extract_format=0 materials/extract_path="" -_subresources={} +_subresources={ +"materials": { +"openPBR_shader1": { +"use_external/enabled": true, +"use_external/fallback_path": "res://assets/FreeLowPolyHouseInterior_NoAlpha/General/Floor/openPBR_shader1.tres", +"use_external/path": "uid://dfkeo8fromug6" +} +} +} gltf/naming_version=2 gltf/embedded_image_handling=1 gltf/texture_map_mode=1 diff --git a/project.godot b/project.godot index 97f72ab..6e5ab4d 100644 --- a/project.godot +++ b/project.godot @@ -36,7 +36,7 @@ project/assembly_name="agame" [editor_plugins] -enabled=PackedStringArray("res://addons/asset_drawer/plugin.cfg", "res://addons/csg_blockout/plugin.cfg", "res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/surface_snap/plugin.cfg") +enabled=PackedStringArray("res://addons/csg_blockout/plugin.cfg", "res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/surface_snap/plugin.cfg") [input]