Unravel Engine C++ Reference
Loading...
Searching...
No Matches
content_browser_panel.cpp
Go to the documentation of this file.
2#include "../panel.h"
3#include "../panels_defs.h"
5#include "imgui_widgets/utils.h"
11#include <editor/shortcuts.h>
27#include <engine/ui/ui_tree.h>
31
33#include <engine/engine.h>
35
36#include <filedialog/filedialog.h>
37#include <filesystem/watcher.h>
38#include <filesystem>
39#include <hpp/utility.hpp>
40#include <imgui/imgui.h>
41#include <imgui/imgui_internal.h>
42#include <imgui_widgets/imcoolbar.h>
43#include <logging/logging.h>
45
46namespace unravel
47{
48using namespace std::literals;
49namespace
50{
51
52fs::path pending_rename;
53
54auto get_new_file(const fs::path& path, const std::string& name, const std::string& ext = "") -> fs::path
55{
56 int i = 0;
57 fs::error_code err;
58 while(fs::exists(path / (fmt::format("{} ({})", name.c_str(), i) + ext), err))
59 {
60 ++i;
61 }
62
63 return path / (fmt::format("{} ({})", name.c_str(), i) + ext);
64}
65
66auto get_new_file_simple(const fs::path& path, const std::string& name, const std::string& ext = "") -> fs::path
67{
68 int i = 0;
69 fs::error_code err;
70 while(fs::exists(path / (fmt::format("{}{}", name.c_str(), i) + ext), err))
71 {
72 ++i;
73 }
74
75 return path / (fmt::format("{}{}", name.c_str(), i) + ext);
76}
77
78auto process_drag_drop_source(const gfx::texture::ptr& preview, const fs::path& absolute_path) -> bool
79{
80 if(ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID))
81 {
82 const auto filename = absolute_path.filename();
83 const std::string extension = filename.has_extension() ? filename.extension().string() : "folder";
84 const std::string id = absolute_path.string();
85 const std::string strfilename = filename.string();
86 ImVec2 item_size = {64, 64};
87 ImVec2 texture_size = ImGui::GetSize(preview);
88 texture_size = ImMax(texture_size, item_size);
89
90 ImGui::ContentItem citem{};
91 citem.texId = ImGui::ToId(preview);
92 citem.name = strfilename.c_str();
93 citem.texture_size = texture_size;
94 citem.image_size = item_size;
95
96 ImGui::ContentButtonItem(citem);
97
98 ImGui::SetDragDropPayload(extension.c_str(), id.data(), id.size());
99 ImGui::EndDragDropSource();
100 return true;
101 }
102
103 return false;
104}
105
106void process_drag_drop_target(const fs::path& absolute_path)
107{
108 if(ImGui::BeginDragDropTarget())
109 {
110 if(ImGui::IsDragDropPayloadBeingAccepted())
111 {
112 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
113 }
114 else
115 {
116 ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed);
117 }
118
119 fs::error_code err;
120 if(fs::is_directory(absolute_path, err))
121 {
122 static const auto types = ex::get_all_formats();
123
124 const auto process_drop = [&absolute_path](const std::string& type)
125 {
126 auto payload = ImGui::AcceptDragDropPayload(type.c_str());
127 if(payload != nullptr)
128 {
129 std::string data(reinterpret_cast<const char*>(payload->Data), std::size_t(payload->DataSize));
130 fs::path new_name = absolute_path / fs::path(data).filename();
131 if(data != new_name)
132 {
133 fs::error_code err;
134
135 if(!fs::exists(new_name, err))
136 {
137 fs::rename(data, new_name, err);
138 }
139 }
140 }
141 return payload;
142 };
143
144 for(const auto& asset_set : types)
145 {
146 for(const auto& type : asset_set)
147 {
148 if(process_drop(type) != nullptr)
149 {
150 break;
151 }
152 }
153 }
154 {
155 process_drop("folder");
156 }
157 {
158 {
159 auto payload = ImGui::AcceptDragDropPayload("entity");
160 if(payload != nullptr)
161 {
162 entt::handle dropped{};
163 std::memcpy(&dropped, payload->Data, size_t(payload->DataSize));
164 if(dropped)
165 {
166 auto& ctx = engine::context();
167 auto& em = ctx.get_cached<editing_manager>();
168
169 auto do_action = [&](entt::handle dropped)
170 {
171 auto& comp = dropped.get<tag_component>();
172 auto prefab_path = absolute_path / fs::path(comp.name + ".pfb").make_preferred();
173 asset_writer::atomic_save_to_file(prefab_path.string(), dropped);
174
175 auto& am = ctx.get_cached<asset_manager>();
176 auto key = fs::convert_to_protocol(prefab_path);
177 dropped.get_or_emplace<prefab_component>().source = am.get_asset<prefab>(key.generic_string());
178 };
179
180
181 if(em.is_selected(dropped))
182 {
183 for(auto e : em.try_get_selections_as<entt::handle>())
184 {
185 if(e)
186 {
187 do_action(*e);
188 }
189 }
190 }
191 else
192 {
193 do_action(dropped);
194 }
195
196 }
197 }
198 }
199 }
200 }
201 ImGui::EndDragDropTarget();
202 }
203}
204
205auto draw_item(const content_browser_item& item)
206{
207 bool is_directory = item.entry.entry.is_directory();
208 const auto& absolute_path = item.entry.entry.path();
209 const auto& name = item.entry.stem;
210 const auto& filename = item.entry.filename;
211 const auto& file_ext = item.entry.extension;
212 const auto& file_type = ex::get_type(file_ext, is_directory);
213 enum class entry_action
214 {
215 none,
216 clicked,
217 double_clicked,
218 renamed,
219 deleted,
220 duplicate,
221 };
222
223 auto duplicate_entry = [&]()
224 {
225 fs::error_code err;
226 const auto available = get_new_file(absolute_path.parent_path(), name, file_ext);
227 fs::copy(absolute_path, available, fs::copy_options::overwrite_existing, err);
228 };
229
230 bool is_popup_opened = false;
231 entry_action action = entry_action::none;
232
233 bool open_rename_menu = false;
234
235 ImGui::PushID(name.c_str());
236 if(item.is_selected && !ImGui::IsAnyItemActive() && ImGui::IsWindowFocused())
237 {
238 if(ImGui::IsKeyPressed(shortcuts::rename_item))
239 {
240 open_rename_menu = true;
241 }
242
243 if(ImGui::IsKeyPressed(shortcuts::delete_item))
244 {
245 action = entry_action::deleted;
246 }
247
248 if(ImGui::IsItemCombinationKeyPressed(shortcuts::duplicate_item))
249 {
250 action = entry_action::duplicate;
251 }
252 }
253
254 bool is_editing_label_after_create = pending_rename == absolute_path;
255 if(is_editing_label_after_create)
256 {
257 open_rename_menu = true;
258 }
259
260 ImVec2 item_size = {item.size, item.size};
261 ImVec2 texture_size = ImGui::GetSize(item.icon, item_size);
262
263 auto pos = ImGui::GetCursorScreenPos();
264 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
265
266 auto file_type_font = ImGui::GetFont(ImGui::Font::Black);
267
268 ImGui::ContentItem citem{};
269 citem.texId = ImGui::ToId(item.icon);
270 citem.name = name.c_str();
271 citem.type = file_type.c_str();
272 citem.type_font = file_type_font;
273 citem.texture_size = texture_size;
274 citem.image_size = item_size;
275
276 // Track double-click state across frames
277 static ImGuiID last_double_clicked_id = 0;
278 static float last_double_click_time = -1.0f;
279 const float double_click_timeout = 0.5f; // seconds
280
281 ImGuiID current_id = ImGui::GetID(name.c_str());
282 float current_time = ImGui::GetTime();
283
284 bool button_clicked = false;//ImGui::ContentButtonItem(citem);
285
286 if(!item.is_loading)
287 {
288 button_clicked = ImGui::ContentButtonItem(citem);
289 ImGui::DrawItemActivityOutline(ImGui::OutlineFlags_All);
290
291 }
292 else
293 {
294
295 auto spinner_size = item_size.x;
296
297 ImSpinner::Spinner<ImSpinner::SpinnerTypeT::e_st_eclipse>("spinner",
298 ImSpinner::Radius{spinner_size * 0.5f},
299 ImSpinner::Thickness{6.0f},
300 ImSpinner::Color{ImSpinner::white},
301 ImSpinner::Speed{6.0f});
302 }
303
304 pos.y += ImGui::GetItemRectSize().y;
305
306 ImGui::PopStyleVar();
307
308 // Check for double-click
309 bool is_double_clicked = ImGui::IsItemDoubleClicked(ImGuiMouseButton_Left);
310 if(is_double_clicked)
311 {
312 last_double_clicked_id = current_id;
313 last_double_click_time = current_time;
314 action = entry_action::double_clicked;
315 }
316 // Only handle regular click if it's not a double-click and not recently double-clicked
317 else if(button_clicked &&
318 !(last_double_clicked_id == current_id &&
319 current_time - last_double_click_time < double_click_timeout))
320 {
321 action = entry_action::clicked;
322 }
323
324 // Check if this item just received focus through keyboard navigation
325 if(ImGui::IsItemFocused())
326 {
327 // Use the new IsItemFocusChanged function to detect navigation focus changes
328 if(ImGui::IsItemFocusChanged() && !item.is_selected)
329 {
330 APPLOG_INFO("Focus Changed");
331
332 // Only trigger click when the item wasn't previously selected
333 action = entry_action::clicked;
334 }
335
336 if(ImGui::IsKeyPressed(shortcuts::item_action) || ImGui::IsKeyPressed(shortcuts::item_action_alt))
337 {
338 action = entry_action::double_clicked;
339 }
340
341 if(ImGui::IsKeyPressed(shortcuts::item_cancel))
342 {
343 action = entry_action::none;
344 }
345 }
346
347 if(ImGui::IsItemHovered())
348 {
349 if(item.on_double_click)
350 {
351 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
352 }
353 }
354
355 ImGui::AddItemTooltipEx("%s", filename.c_str());
356
357 if(!file_type.empty())
358 {
359 ImGui::PushFont(file_type_font, file_type_font->LegacySize);
360 ImGui::AddItemTooltipEx("%s", file_type.c_str());
361 ImGui::PopFont();
362 }
363
364 auto input_buff = ImGui::CreateInputTextBuffer(name);
365
366 if(ImGui::BeginPopupContextItem("ENTRY_CONTEXT_MENU"))
367 {
368 is_popup_opened = true;
369
370 if(ImGui::Selectable("Open in Explorer"))
371 {
372 fs::show_in_graphical_env(absolute_path);
373 }
374 if(ImGui::MenuItem("Rename", ImGui::GetKeyName(shortcuts::rename_item)))
375 {
376 open_rename_menu = true;
377 ImGui::CloseCurrentPopup();
378 }
379
380 if(ImGui::MenuItem("Duplicate", ImGui::GetKeyCombinationName(shortcuts::duplicate_item).c_str()))
381 {
382 action = entry_action::duplicate;
383 ImGui::CloseCurrentPopup();
384 }
385
386 if(ImGui::MenuItem("Delete", ImGui::GetKeyName(shortcuts::delete_item)))
387 {
388 action = entry_action::deleted;
389 ImGui::CloseCurrentPopup();
390 }
391 ImGui::EndPopup();
392 }
393
394 const float rename_field_width = 150.0f;
395 if(open_rename_menu)
396 {
397 ImGui::OpenPopup("ENTRY_RENAME_MENU");
398
399 const auto& style = ImGui::GetStyle();
400 float rename_field_with_padding = rename_field_width + style.WindowPadding.x * 2.0f;
401 if(item.size < rename_field_with_padding)
402 {
403 auto diff = rename_field_with_padding - item.size;
404 pos.x -= diff * 0.5f;
405 }
406
407 ImGui::SetNextWindowPos(pos);
408 }
409
410 if(ImGui::BeginPopup("ENTRY_RENAME_MENU"))
411 {
412 is_popup_opened = true;
413 if(open_rename_menu)
414 {
415 ImGui::SetKeyboardFocusHere();
416 }
417 ImGui::PushItemWidth(rename_field_width);
418
419 if(ImGui::InputTextWidget("##NAME",
420 input_buff,
421 false,
422 ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
423 {
424 action = entry_action::renamed;
425 ImGui::CloseCurrentPopup();
426 }
427
428 if(open_rename_menu)
429 {
430 ImGui::ActivateItemByID(ImGui::GetItemID());
431 }
432
433 if(is_editing_label_after_create && ImGui::IsItemKeyPressed(shortcuts::item_cancel))
434 {
435 action = entry_action::deleted;
436 }
437
438 ImGui::PopItemWidth();
439 ImGui::EndPopup();
440 }
441 if(item.is_selected)
442 {
443 ImGui::SetItemFocusFrame();
444 }
445
446 if(item.is_focused)
447 {
448 ImGui::SetItemFocusFrame(ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 1.0f)));
449 }
450
451 if(item.is_loading)
452 {
453 action = entry_action::none;
454 }
455
456 if(open_rename_menu)
457 {
458 if(item.on_click)
459 {
460 item.on_click();
461 }
462 }
463 switch(action)
464 {
465 case entry_action::clicked:
466 {
467 pending_rename.clear();
468 if(item.on_click)
469 {
470 item.on_click();
471 }
472 }
473 break;
474 case entry_action::double_clicked:
475 {
476 pending_rename.clear();
477
478 if(item.on_double_click)
479 {
480 item.on_double_click();
481 }
482 }
483 break;
484 case entry_action::renamed:
485 {
486 pending_rename.clear();
487
488 const std::string new_name = std::string(input_buff.data());
489 if(new_name != name && !new_name.empty())
490 {
491 if(item.on_rename)
492 {
493 item.on_rename(new_name);
494 }
495 }
496 }
497 break;
498 case entry_action::deleted:
499 {
500 pending_rename.clear();
501
502 if(item.on_delete)
503 {
504 item.on_delete();
505 }
506 }
507 break;
508
509 case entry_action::duplicate:
510 {
511 pending_rename.clear();
512 duplicate_entry();
513 }
514 break;
515
516 default:
517 break;
518 }
519
520 if(!process_drag_drop_source(item.icon, absolute_path))
521 {
522 process_drag_drop_target(absolute_path);
523 }
524
525 ImGui::PopID();
526 return is_popup_opened;
527}
528
529} // namespace
536
538{
539 filter_ = {};
540}
541
543{
544 if(ImGui::Begin(name, nullptr))
545 {
546 // ImGui::WindowTimeBlock block(ImGui::GetFont(ImGui::Font::Mono));
547
548 draw(ctx);
549
550 handle_external_drop(ctx);
551 }
552 ImGui::End();
553}
554
555void content_browser_panel::handle_external_drop(rtti::context& ctx)
556{
557 if(!parent_->get_external_drop_in_progress())
558 {
559 const auto& files = parent_->get_external_drop_files();
560 if(!files.empty())
561 {
562 on_import(ctx, files, cache_.get_path());
563
564 parent_->clear_external_drop_files();
565 }
566 }
567}
568
569void content_browser_panel::draw(rtti::context& ctx)
570{
571 auto& em = ctx.get_cached<editing_manager>();
572
573 const auto root_path = fs::resolve_protocol("app:/data");
574
575 fs::error_code err;
576 if(root_ != root_path || !fs::exists(cache_.get_path(), err))
577 {
578 root_ = root_path;
579 set_cache_path(root_);
580 }
581
582 if(!em.focused_data.focus_path.empty())
583 {
584 set_cache_path(em.focused_data.focus_path);
585 em.focused_data.focus_path.clear();
586 }
587
588 auto avail = ImGui::GetContentRegionAvail();
589 if(avail.x < 1.0f || avail.y < 1.0f)
590 {
591 return;
592 }
593
594 if(ImGui::BeginChild("DETAILS_AREA",
595 avail * ImVec2(0.15f, 1.0f),
596 ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX))
597 {
598 // ImGui::WindowTimeBlock block(ImGui::GetFont(ImGui::Font::Mono));
599
600 if(fs::is_directory(root_path, err))
601 {
602 draw_details(ctx, root_path);
603 }
604 }
605 ImGui::EndChild();
606
607 ImGui::SameLine();
608
609 if(ImGui::BeginChild("EXPLORER"))
610 {
611 // ImGui::WindowTimeBlock block(ImGui::GetFont(ImGui::Font::Mono));
612 draw_as_explorer(ctx, root_path);
613 }
614 ImGui::EndChild();
615
616 const auto& current_path = cache_.get_path();
617 process_drag_drop_target(current_path);
618
619 if(refresh_ > 0)
620 {
621 refresh_--;
622 }
623}
624
625void content_browser_panel::draw_details(rtti::context& ctx, const fs::path& path)
626{
627 {
628 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth;
629
630 const auto& selected_path = cache_.get_path();
631 if(selected_path == path)
632 {
633 flags |= ImGuiTreeNodeFlags_Selected;
634 }
635
636 if(refresh_ > 0 && (path == selected_path || fs::is_any_parent_path(path, selected_path)))
637 {
638 ImGui::SetNextItemOpen(true);
639 }
640
641 auto stem = path.stem();
642 bool open = ImGui::TreeNodeEx(fmt::format("{} {}", ICON_MDI_FOLDER, stem.generic_string()).c_str(), flags);
643 process_drag_drop_target(path);
644
645 // Add context menu for the folder item using the refactored function
646 context_menu(ctx, true, path);
647
648 const bool clicked = !ImGui::IsItemToggledOpen() && ImGui::IsItemClicked(ImGuiMouseButton_Left);
649
650 // Use the new IsItemFocusChanged function to detect navigation focus changes
651 if (ImGui::IsItemFocused() && ImGui::IsItemFocusChanged())
652 {
653 // Item just received focus through keyboard navigation
654 set_cache_path(path);
655 }
656
657 if(open)
658 {
659 const fs::directory_iterator it(path);
660 for(const auto& p : it)
661 {
662 if(fs::is_directory(p.status()))
663 {
664 const auto& path = p.path();
665 draw_details(ctx, path);
666 }
667 }
668
669 ImGui::TreePop();
670 }
671
672 if(clicked)
673 {
674 set_cache_path(path);
675 }
676 }
677}
678
679void content_browser_panel::draw_as_explorer(rtti::context& ctx, const fs::path& root_path)
680{
681 auto& am = ctx.get_cached<asset_manager>();
682 auto& em = ctx.get_cached<editing_manager>();
683 auto& tm = ctx.get_cached<thumbnail_manager>();
684
685 const float size = ImGui::GetFrameHeight() * 6.0f * scale_;
686 const auto hierarchy = fs::split_until(cache_.get_path(), root_path);
687
688 // Handle backspace key to navigate to parent directory
689 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() &&
690 ImGui::IsKeyPressed(shortcuts::navigate_back) &&
691 hierarchy.size() > 1)
692 {
693 // Navigate to parent directory
694 fs::path parent_path = cache_.get_path().parent_path();
695 if (fs::exists(parent_path) && parent_path != cache_.get_path())
696 {
697 set_cache_path(parent_path);
698 }
699 }
700
701 ImGui::DrawFilterWithHint(filter_, ICON_MDI_FILE_SEARCH " Search...", 200.0f);
702 ImGui::DrawItemActivityOutline();
703 ImGui::SameLine();
704 ImGui::Text("%s", ICON_MDI_HOME);
705 ImGui::SameLine(0.0f, 0.0f);
706 int id = 0;
707 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
708 ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0.0f, 0.0f));
709
710 for(const auto& dir : hierarchy)
711 {
712 const bool is_first = &dir == &hierarchy.front();
713 const bool is_last = &dir == &hierarchy.back();
714 ImGui::PushID(id++);
715
716 if(!is_first)
717 {
718 ImGui::SameLine(0.0f, 0.0f);
719 ImGui::AlignTextToFramePadding();
720 ImGui::TextUnformatted("/");
721 ImGui::SameLine(0.0f, 0.0f);
722 }
723
724 if(is_last)
725 {
727 }
728
729 auto filename = dir.filename().string();
730 if(is_first)
731 {
732 filename = fmt::format("app:/{}", filename);
733 }
734 const bool clicked = ImGui::Button(filename.c_str());
735
736 if(is_last)
737 {
738 ImGui::PopFont();
739 }
740 ImGui::PopID();
741
742 if(clicked)
743 {
744 set_cache_path(dir);
745 break;
746 }
747 process_drag_drop_target(dir);
748 }
749 ImGui::PopStyleVar(2);
750
751
752 ImGui::SameLine(0.0f, 0.0f);
753 ImGui::AlignedItem(1.0f,
754 ImGui::GetContentRegionAvail().x,
755 80.0f,
756 [&]()
757 {
758 ImGui::PushItemWidth(80.0f);
759 ImGui::SliderFloat("##scale", &scale_, 0.5f, 1.0f);
760 ImGui::SetItemTooltipEx("%s", "Icons scale");
761 ImGui::PopItemWidth();
762 });
763
764 ImGui::Separator();
765
766 ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
767 ImGuiWindowFlags_NoSavedSettings;
768
769 fs::path current_path = cache_.get_path();
770
771 if(ImGui::BeginChild("assets_content", ImGui::GetContentRegionAvail(), false, flags))
772 {
774
775 bool is_popup_opened = false;
776
777
778 auto process_cache_entry = [&, this](const auto& cache_entry)
779 {
780 const auto& absolute_path = cache_entry.entry.path();
781 const auto& name = cache_entry.stem;
782 const auto& filename = cache_entry.filename;
783 const auto& relative = cache_entry.protocol_path;
784 const auto& file_ext = cache_entry.extension;
785
786 content_browser_item item(cache_entry);
787 item.size = size;
788
789 // Use reusable rename handler
790 setup_rename_handler(item, absolute_path, file_ext);
791
792 bool known = false;
793 hpp::for_each_type<gfx::texture,
795 scene_prefab,
796 material,
797 physics_material,
798 ui_tree,
799 style_sheet,
800 audio_clip,
801 mesh,
802 prefab,
803 animation_clip,
804 font,
805 script>(
806 [&](auto tag)
807 {
808 if(known)
809 {
810 return;
811 }
812
813 using asset_t = typename std::decay_t<decltype(tag)>::type;
814
815 if(ex::is_format<asset_t>(file_ext))
816 {
817 known = true;
818 setup_asset_item<asset_t>(ctx, item, absolute_path, relative, file_ext);
819 is_popup_opened |= draw_item(item);
820 }
821 });
822
823 if(!known)
824 {
825 fs::error_code ec;
826 using entry_t = fs::path;
827 const entry_t& entry = absolute_path;
828 item.icon = tm.get_thumbnail(entry);
829 item.is_selected = em.is_selected(entry);
830 item.is_focused = em.is_focused(entry);
831
832 item.on_click = [&em, entry]()
833 {
834 em.select(entry, em.get_select_mode());
835 };
836
837 // Use reusable template delete handler for unknown assets
838 setup_delete_handler(item, relative, absolute_path, entry, ctx);
839
840 // Use reusable rename handler
841 setup_rename_handler(item, absolute_path, file_ext);
842
843 if(fs::is_directory(cache_entry.entry.status()))
844 {
845 item.on_double_click = [&current_path, &em, entry]()
846 {
847 current_path = entry;
848 em.try_unselect<entry_t>();
849 };
850 }
851
852 is_popup_opened |= draw_item(item);
853 }
854 };
855
856 auto cache_size = cache_.size();
857
858 if(!filter_.IsActive())
859 {
860 ImGui::ItemBrowser(size,
861 cache_size,
862 [&](int index)
863 {
864 auto& cache_entry = cache_[index];
865 process_cache_entry(cache_entry);
866 });
867 }
868 else
869 {
870 std::vector<fs::directory_cache::cache_entry> filtered_entries;
871 for(size_t index = 0; index < cache_size; ++index)
872 {
873 const auto& cache_entry = cache_[index];
874
875 const auto& name = cache_entry.stem;
876 const auto& filename = cache_entry.filename;
877 const auto& extension = cache_entry.extension;
878
879 if(filter_.PassFilter(name.c_str()) ||
880 filter_.PassFilter(ex::get_type(extension, cache_entry.entry.is_directory()).c_str()))
881 {
882 filtered_entries.emplace_back(cache_entry);
883 }
884
885 }
886
887 ImGui::ItemBrowser(size,
888 filtered_entries.size(),
889 [&](int index)
890 {
891 auto& cache_entry = filtered_entries[index];
892 process_cache_entry(cache_entry);
893 });
894 }
895
896 if(!is_popup_opened)
897 {
898 context_menu(ctx, false, cache_.get_path());
899 }
900 set_cache_path(current_path);
901
903
904 handle_window_empty_click(ctx);
905 }
906 ImGui::EndChild();
907}
908
909void content_browser_panel::handle_window_empty_click(rtti::context& ctx) const
910{
911 auto& em = ctx.get_cached<editing_manager>();
912 if(ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
913 {
914 if(!ImGui::IsAnyItemHovered())
915 {
916 em.unselect();
917 }
918 }
919}
920
921void content_browser_panel::context_menu(rtti::context& ctx, bool use_context_item, const fs::path& target_path)
922{
923 bool popup_opened = false;
924
925 if (use_context_item)
926 {
927 popup_opened = ImGui::BeginPopupContextItem();
928 }
929 else
930 {
931 popup_opened = ImGui::BeginPopupContextWindowEx();
932 }
933
934 if (popup_opened)
935 {
936 set_cache_path(target_path);
937
938 context_create_menu(ctx, target_path);
939
940 ImGui::Separator();
941
942 if(ImGui::Selectable("Open in Explorer"))
943 {
944 fs::show_in_graphical_env(target_path);
945 }
946
947 ImGui::Separator();
948
949 if(ImGui::Selectable("Import..."))
950 {
951 import(ctx, target_path);
952 }
953 ImGui::SetItemTooltipEx("If import asset consists of multiple files,\n"
954 "just copy paste all the files the data folder.\n"
955 "Preferably in a new folder. The importer will\n"
956 "automatically pick them up as dependencies.");
957
958 ImGui::EndPopup();
959 }
960}
961
962void content_browser_panel::context_create_menu(rtti::context& ctx, const fs::path& target_path)
963{
964 if(ImGui::BeginMenu("Create"))
965 {
966 if(ImGui::MenuItem("Folder"))
967 {
968 const auto available = get_new_file(target_path, "New Folder");
969 fs::error_code ec;
970 fs::create_directory(available, ec);
971
972 if(!ec)
973 {
974 pending_rename = available;
975 }
976 }
977
978 ImGui::Separator();
979
980 if(ImGui::MenuItem("C# Script"))
981 {
982 auto& am = ctx.get_cached<asset_manager>();
983
984 const auto available =
985 get_new_file_simple(target_path, "NewScriptComponent", ex::get_format<script>());
986
987 fs::error_code ec;
988 auto new_script_template =
989 fs::resolve_protocol("engine:/data/scripts/template/TemplateComponent" + ex::get_format<script>());
990 fs::copy(new_script_template, available, ec);
991
992 if(!ec)
993 {
994 pending_rename = available;
995 }
996 }
997
998 ImGui::Separator();
999
1000 if(ImGui::MenuItem(ex::get_type<material>().c_str()))
1001 {
1002 auto& am = ctx.get_cached<asset_manager>();
1003
1004 auto new_name = fmt::format("New {}", ex::get_type<material>());
1005 const auto available = get_new_file(target_path, new_name, ex::get_format<material>());
1006 const auto key = fs::convert_to_protocol(available).generic_string();
1007
1008 auto new_mat_future = am.get_asset_from_instance<material>(key, std::make_shared<pbr_material>());
1009 asset_writer::atomic_save_to_file(new_mat_future.id(), new_mat_future);
1010
1011 {
1012 pending_rename = available;
1013 }
1014 }
1015
1016 if(ImGui::MenuItem(ex::get_type<physics_material>().c_str()))
1017 {
1018 auto& am = ctx.get_cached<asset_manager>();
1019
1020 auto new_name = fmt::format("New {}", ex::get_type<physics_material>());
1021 const auto available =
1022 get_new_file(target_path, new_name, ex::get_format<physics_material>());
1023 const auto key = fs::convert_to_protocol(available).generic_string();
1024
1025 auto new_mat_future =
1026 am.get_asset_from_instance<physics_material>(key, std::make_shared<physics_material>());
1027 asset_writer::atomic_save_to_file(new_mat_future.id(), new_mat_future);
1028
1029 {
1030 pending_rename = available;
1031 }
1032 }
1033
1034 ImGui::Separator();
1035
1036 if(ImGui::MenuItem(ex::get_type<ui_tree>().c_str()))
1037 {
1038 auto& am = ctx.get_cached<asset_manager>();
1039
1040 auto new_name = fmt::format("New {}", ex::get_type<ui_tree>());
1041 const auto available =
1042 get_new_file(target_path, new_name, ex::get_format<ui_tree>());
1043 const auto key = fs::convert_to_protocol(available).generic_string();
1044
1045
1046 fs::error_code err;
1048 available,
1049 [&](const fs::path& temp)
1050 {
1051 fs::error_code ec;
1052 fs::copy(fs::resolve_protocol("engine:/data/ui/template.rhtml"), available, ec);
1053 },
1054 err);
1055
1056 {
1057 pending_rename = available;
1058 }
1059 }
1060
1061 if(ImGui::MenuItem(ex::get_type<style_sheet>().c_str()))
1062 {
1063 auto& am = ctx.get_cached<asset_manager>();
1064
1065 auto new_name = fmt::format("New {}", ex::get_type<style_sheet>());
1066 const auto available =
1067 get_new_file(target_path, new_name, ex::get_format<style_sheet>());
1068 const auto key = fs::convert_to_protocol(available).generic_string();
1069
1070 fs::error_code err;
1072 available,
1073 [&](const fs::path& temp)
1074 {
1075 fs::error_code ec;
1076 fs::copy(fs::resolve_protocol("engine:/data/ui/template.rcss"), available, ec);
1077 },
1078 err);
1079
1080 {
1081 pending_rename = available;
1082 }
1083 }
1084
1085 ImGui::EndMenu();
1086 }
1087}
1088
1089void content_browser_panel::set_cache_path(const fs::path& path)
1090{
1091 if(cache_.get_path() == path)
1092 {
1093 return;
1094 }
1095
1096 auto resolved = fs::resolve_protocol("app:/data");
1097
1098
1099 fs::error_code ec;
1100 if(!fs::equivalent(resolved, path, ec))
1101 {
1102 if(!fs::is_any_parent_path(resolved, path))
1103 {
1104 return;
1105 }
1106 }
1107
1108
1109 if(!fs::exists(path, ec))
1110 {
1111 return;
1112 }
1113
1114
1115 fs::pattern_filter filter;
1116 filter.add_include_pattern("*");
1118 cache_.set_path(path, filter);
1119 refresh_ = 3;
1120}
1121
1122void content_browser_panel::import(rtti::context& ctx, const fs::path& target_path)
1123{
1124 std::vector<std::string> paths;
1125 if(native::open_files_dialog(paths, {}))
1126 {
1127 on_import(ctx, paths, target_path);
1128 }
1129}
1130
1131void content_browser_panel::on_import(rtti::context& ctx, const std::vector<std::string>& paths, const fs::path& target_path)
1132{
1133 auto& ts = ctx.get_cached<threader>();
1134
1135 for(auto& path : paths)
1136 {
1137 fs::path p = fs::path(path).make_preferred();
1138 fs::path filename = p.filename();
1139
1140 APPLOG_INFO("Importing {0}", filename.string());
1141 auto task = ts.pool->schedule("Importing " + filename.extension().string(),
1142 [target_path](const fs::path& path, const fs::path& filename)
1143 {
1144 fs::error_code err;
1145 fs::path dir = target_path / filename;
1146 asset_writer::atomic_copy_file(path, dir, err);
1147 },
1148 p,
1149 filename);
1150 }
1151}
1152
1153void content_browser_panel::prompt_delete_asset(const std::string& name, const std::function<void()>& on_delete)
1154{
1155 ImBox::ShowDeleteConfirmation("Delete selected asset?",
1156 fmt::format("{}\n\nYou cannot undo the delete asset action.", name),
1157 [on_delete](ImBox::ModalResult result)
1158 {
1159 if(result == ImBox::ModalResult::Delete)
1160 {
1161 on_delete();
1162 }
1163 });
1164}
1165
1166template<typename EntryType>
1167void content_browser_panel::setup_delete_handler(content_browser_item& item, const std::string& relative,
1168 const fs::path& absolute_path, const EntryType& entry, rtti::context& ctx)
1169{
1170 auto& em = ctx.get_cached<editing_manager>();
1171
1172 item.on_delete = [this, relative, absolute_path, &em, entry]()
1173 {
1174 auto delete_impl = [&em, absolute_path, entry]()
1175 {
1176 fs::error_code err;
1177 fs::remove_all(absolute_path, err);
1178 em.unselect(entry); // Works for both asset handles and fs::path
1179 };
1180
1181 this->prompt_delete_asset(relative, delete_impl);
1182 };
1183}
1184
1185void content_browser_panel::setup_rename_handler(content_browser_item& item, const fs::path& absolute_path,
1186 const std::string& file_ext)
1187{
1188 item.on_rename = [absolute_path, file_ext](const std::string& new_name)
1189 {
1190 fs::path new_absolute_path = absolute_path;
1191 new_absolute_path.remove_filename();
1192 new_absolute_path /= new_name + file_ext;
1193 fs::error_code err;
1194 fs::rename(absolute_path, new_absolute_path, err);
1195 };
1196}
1197
1198template<typename AssetType>
1199void content_browser_panel::setup_asset_item(rtti::context& ctx, content_browser_item& item,
1200 const fs::path& absolute_path,
1201 const std::string& relative,
1202 const std::string& file_ext)
1203{
1204 auto& am = ctx.get_cached<asset_manager>();
1205 auto& em = ctx.get_cached<editing_manager>();
1206 auto& tm = ctx.get_cached<thumbnail_manager>();
1207
1208 using entry_t = asset_handle<AssetType>;
1209 const auto& entry = am.find_asset<AssetType>(relative);
1210
1211 item.icon = tm.get_thumbnail(entry);
1212 item.is_selected = em.is_selected(entry);
1213 item.is_focused = em.is_focused(entry);
1214 item.is_loading = !entry.is_ready();
1215
1216 // Simple click handler
1217 item.on_click = [&em, entry]()
1218 {
1219 em.select(entry, em.get_select_mode());
1220 };
1221
1222 // Use reusable template delete handler
1223 setup_delete_handler(item, relative, absolute_path, entry, ctx);
1224
1225 // Use reusable rename handler
1226 setup_rename_handler(item, absolute_path, file_ext);
1227
1228 // Set up double-click handlers based on asset type
1229 if constexpr(std::is_same_v<AssetType, scene_prefab>)
1230 {
1231 item.on_double_click = [&ctx, entry]()
1232 {
1234 };
1235 }
1236 else if constexpr(std::is_same_v<AssetType, prefab>)
1237 {
1238 item.on_double_click = [this, &ctx, entry]()
1239 {
1240 auto& em_local = ctx.get_cached<editing_manager>();
1241 auto& scene_panel = parent_->get_scene_panel();
1242
1243 bool auto_save = scene_panel.get_auto_save_prefab();
1244 em_local.enter_prefab_mode(ctx, entry, auto_save);
1245 };
1246 }
1247 else if constexpr(std::is_same_v<AssetType, script> ||
1248 std::is_same_v<AssetType, gfx::shader> ||
1249 std::is_same_v<AssetType, style_sheet>)
1250 {
1251 item.on_double_click = [absolute_path]()
1252 {
1254 };
1255 }
1256 // For other asset types, no double-click action for now
1257}
1258
1259} // namespace unravel
manifold_type type
const fs::path & get_path() const
Definition cache.hpp:194
void set_path(const fs::path &path, const fs::pattern_filter &filter)
Definition cache.hpp:199
decltype(auto) size() const
Returns the size for the underlying cached container.
Definition cache.hpp:126
A filter that combines include and exclude patterns for file/directory filtering.
void add_exclude_pattern(const std::string &pattern)
Adds an exclude pattern to the filter.
void add_include_pattern(const std::string &pattern)
Adds an include pattern to the filter.
void on_frame_ui_render(rtti::context &ctx, const char *name)
auto get_scene_panel() -> scene_panel &
Definition panel.cpp:156
void clear_external_drop_files()
Definition panel.cpp:211
auto get_external_drop_files() const -> const std::vector< std::string > &
Definition panel.cpp:216
auto get_external_drop_in_progress() const -> bool
Definition panel.cpp:191
ImGui::Font::Enum font
Definition hub.cpp:24
std::string name
Definition hub.cpp:27
std::string tag
Definition hub.cpp:26
#define ICON_MDI_FOLDER
#define ICON_MDI_HOME
#define ICON_MDI_FILE_SEARCH
#define APPLOG_INFO(...)
Definition logging.h:18
ModalResult
Modal result flags for message box buttons.
auto ShowDeleteConfirmation(const std::string &title, const std::string &message, std::function< void(ModalResult)> callback) -> std::shared_ptr< MsgBox >
Show a delete confirmation dialog with Delete/Cancel buttons.
void PushWindowFontSize(int size)
Definition imgui.cpp:666
void PushFont(Font::Enum _font)
Definition imgui.cpp:617
ImTextureID ToId(gfx::texture_handle _handle, uint8_t _mip=0, uint8_t _flags=IMGUI_FLAGS_ALPHA_BLEND)
Definition imgui.h:102
ImFont * GetFont(Font::Enum _font)
Definition imgui.cpp:623
ImVec2 GetSize(const gfx::texture &tex, const ImVec2 &fallback={})
Definition imgui.h:141
void PopWindowFontSize()
Definition imgui.cpp:682
auto get_all_formats() -> const std::vector< std::vector< std::string > > &
auto get_format(bool include_dot=true) -> std::string
auto get_type() -> const std::string &
auto is_format(const std::string &ex) -> bool
auto get_meta_format() -> const std::string &
path resolve_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
bool is_any_parent_path(const path &parent, const path &child)
std::vector< path > split_until(const path &_path, const path &_predicate)
another.
path convert_to_protocol(const path &_path)
Oposite of the resolve_protocol this function tries to convert to protocol path from an absolute one.
auto atomic_save_to_file(const fs::path &key, const asset_handle< T > &obj) -> bool
void atomic_write_file(const fs::path &dst, const std::function< void(const fs::path &)> &callback, fs::error_code &ec) noexcept
constexpr ImGuiKey item_cancel
Definition shortcuts.h:38
constexpr ImGuiKey delete_item
Definition shortcuts.h:33
const ImGuiKeyCombination duplicate_item
Definition shortcuts.h:34
constexpr ImGuiKey rename_item
Definition shortcuts.h:32
constexpr ImGuiKey item_action_alt
Definition shortcuts.h:37
constexpr ImGuiKey item_action
Definition shortcuts.h:36
constexpr ImGuiKey navigate_back
Definition shortcuts.h:35
float x
Represents a handle to an asset, providing access and management functions.
auto get_cached() -> T &
Definition context.hpp:49
size()=default
static void open_workspace_on_file(const fs::path &file, int line=0)
static auto open_scene_from_asset(rtti::context &ctx, const asset_handle< scene_prefab > &asset) -> bool
static auto context() -> rtti::context &
Definition engine.cpp:115