diff --git a/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h b/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h index edffbaa26..e9d701d18 100644 --- a/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h +++ b/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h @@ -35,6 +35,7 @@ namespace OvCore::Helpers std::string tooltip; uint32_t iconID = 0; std::function onSelected; + bool alwaysVisible = false; }; class PickerItemList @@ -60,6 +61,7 @@ namespace OvCore::Helpers using FileItemBuilderCallback = std::function, bool, bool)>; using OpenProviderCallback = std::function; using PickerProviderCallback = std::function; + using PickerSearchTextProviderCallback = std::function; using IconProviderCallback = std::function; using ActorSelectionProviderCallback = std::function; using AssetExistsCallback = std::function; @@ -85,6 +87,9 @@ namespace OvCore::Helpers static void SetPickerProvider(PickerProviderCallback p_provider); static void OpenPicker(PickerItemList p_items, std::string p_title); + static void SetPickerSearchTextProvider(PickerSearchTextProviderCallback p_provider); + static std::string GetPickerSearchText(); + static void SetActorIconID(uint32_t p_id); static uint32_t GetActorIconID(); diff --git a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp index d16efce38..e3d47948d 100644 --- a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp +++ b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp @@ -13,6 +13,7 @@ namespace OvRendering::Resources::Texture* __EMPTY_TEXTURE = nullptr; OvCore::Helpers::GUIHelpers::FileItemBuilderCallback __FILE_ITEM_BUILDER; OvCore::Helpers::GUIHelpers::PickerProviderCallback __PICKER_PROVIDER; + OvCore::Helpers::GUIHelpers::PickerSearchTextProviderCallback __PICKER_SEARCH_TEXT_PROVIDER; OvCore::Helpers::GUIHelpers::IconProviderCallback __ICON_PROVIDER; OvCore::Helpers::GUIHelpers::OpenProviderCallback __OPEN_PROVIDER; OvCore::Helpers::GUIHelpers::ActorSelectionProviderCallback __ACTOR_SELECTION_PROVIDER; @@ -103,6 +104,16 @@ void OvCore::Helpers::GUIHelpers::OpenPicker(PickerItemList p_items, std::string __PICKER_PROVIDER(std::move(p_items), std::move(p_title)); } +void OvCore::Helpers::GUIHelpers::SetPickerSearchTextProvider(PickerSearchTextProviderCallback p_provider) +{ + __PICKER_SEARCH_TEXT_PROVIDER = std::move(p_provider); +} + +std::string OvCore::Helpers::GUIHelpers::GetPickerSearchText() +{ + return __PICKER_SEARCH_TEXT_PROVIDER ? __PICKER_SEARCH_TEXT_PROVIDER() : ""; +} + void OvCore::Helpers::GUIHelpers::SetActorIconID(uint32_t p_id) { __ACTOR_ICON_ID = p_id; diff --git a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h index 233173c6c..5d43ef7eb 100644 --- a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h +++ b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h @@ -311,6 +311,14 @@ namespace OvEditor::Core */ bool ImportAsset(const std::string& p_initialDestinationDirectory); + /** + * Open a save dialog to create a new script file at a user-chosen location. + * Returns the absolute path of the created script, or empty on cancellation. + * @param p_initialDirectory Directory the dialog opens in + * @param p_initialName Suggested filename (without extension) + */ + std::string CreateScript(const std::string& p_initialDirectory, const std::string& p_initialName); + /** * Import an asset at location * @param p_destination diff --git a/Sources/OvEditor/include/OvEditor/Helpers/PickerHelpers.h b/Sources/OvEditor/include/OvEditor/Helpers/PickerHelpers.h index 1ca76f4fa..8dca23d7d 100644 --- a/Sources/OvEditor/include/OvEditor/Helpers/PickerHelpers.h +++ b/Sources/OvEditor/include/OvEditor/Helpers/PickerHelpers.h @@ -24,12 +24,14 @@ namespace OvEditor::Helpers::PickerHelpers * @param p_onSelected Called with the resource path when the item is selected * @param p_searchProject Include project assets * @param p_searchEngine Include engine assets + * @param p_filter Optional predicate; return false to exclude an item (receives resource path) */ void AddFileItems( OvCore::Helpers::GUIHelpers::PickerItemList& p_list, OvTools::Utils::PathParser::EFileType p_fileType, std::function p_onSelected, bool p_searchProject = true, - bool p_searchEngine = true + bool p_searchEngine = true, + std::function p_filter = {} ); } diff --git a/Sources/OvEditor/include/OvEditor/Panels/ItemPicker.h b/Sources/OvEditor/include/OvEditor/Panels/ItemPicker.h index 0186aa35a..ffda67240 100644 --- a/Sources/OvEditor/include/OvEditor/Panels/ItemPicker.h +++ b/Sources/OvEditor/include/OvEditor/Panels/ItemPicker.h @@ -47,6 +47,11 @@ namespace OvEditor::Panels */ void Open(OvCore::Helpers::GUIHelpers::PickerItemList p_items, std::string p_title); + /** + * Returns the current text in the search field. + */ + std::string GetSearchText() const; + private: void _Draw_Impl() override; void Populate(); @@ -65,7 +70,13 @@ namespace OvEditor::Panels OvUI::Widgets::InputFields::InputText* m_searchField = nullptr; OvUI::Widgets::Layout::Group* m_listGroup = nullptr; - /* Each entry: (search key, row widget) */ - std::vector> m_rows; + /* Each entry: search key, always-visible flag, and row widget */ + struct RowEntry + { + std::string key; + bool alwaysVisible; + OvUI::Widgets::Layout::Group* widget; + }; + std::vector m_rows; }; } diff --git a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp index 4806b52cc..eb4b36bb4 100644 --- a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp @@ -76,6 +76,10 @@ void OvEditor::Core::Editor::SetupUI() } ); + OvCore::Helpers::GUIHelpers::SetPickerSearchTextProvider( + [this]() { return m_itemPicker->GetSearchText(); } + ); + OvCore::Helpers::GUIHelpers::SetIconProvider( [this](OvTools::Utils::PathParser::EFileType p_fileType) -> uint32_t { auto* texture = m_context.editorResources->GetTexture(OvTools::Utils::PathParser::FileTypeToString(p_fileType)); diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 0af14c589..6e114f7d4 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -1101,6 +1102,42 @@ bool OvEditor::Core::EditorActions::ImportAsset(const std::string& p_initialDest return false; } +std::string OvEditor::Core::EditorActions::CreateScript(const std::string& p_initialDirectory, const std::string& p_initialName) +{ + using namespace OvWindowing::Dialogs; + + const std::string extension = m_context.scriptEngine->GetDefaultExtension(); + + SaveFileDialog dialog("Create Script"); + dialog.SetInitialDirectory(p_initialDirectory); + dialog.SetInitialFilename(p_initialName); + dialog.DefineExtension("Script", extension); + dialog.Show( + EExplorerFlags::DONTADDTORECENT | + EExplorerFlags::PATHMUSTEXIST | + EExplorerFlags::HIDEREADONLY | + EExplorerFlags::NOCHANGEDIR + ); + + if (!dialog.HasSucceeded()) + return {}; + + const std::string destination = dialog.GetSelectedFilePath(); + + if (!std::filesystem::exists(destination)) + { + const std::string scriptName = std::filesystem::path{ destination }.stem().string(); + const std::string fileContent = m_context.scriptEngine->GetDefaultScriptContent(scriptName); + + std::ofstream outfile(destination); + outfile << fileContent << std::endl; + + EDITOR_PANEL(Panels::AssetBrowser, "Asset Browser").Refresh(); + } + + return destination; +} + bool OvEditor::Core::EditorActions::ImportAssetAtLocation(const std::string& p_destination) { using namespace OvWindowing::Dialogs; diff --git a/Sources/OvEditor/src/OvEditor/Helpers/PickerHelpers.cpp b/Sources/OvEditor/src/OvEditor/Helpers/PickerHelpers.cpp index 6fa8f6ffe..8cde25646 100644 --- a/Sources/OvEditor/src/OvEditor/Helpers/PickerHelpers.cpp +++ b/Sources/OvEditor/src/OvEditor/Helpers/PickerHelpers.cpp @@ -19,7 +19,8 @@ namespace const std::filesystem::path& p_directory, bool p_isEngine, PathParser::EFileType p_fileType, - const std::function& p_onSelected) + const std::function& p_onSelected, + const std::function& p_filter) { if (!std::filesystem::exists(p_directory)) return; @@ -41,6 +42,10 @@ namespace continue; const std::string resourcePath = EDITOR_EXEC(GetResourcePath(path, p_isEngine)); + + if (p_filter && !p_filter(resourcePath)) + continue; + const std::string filename = PathParser::GetElementName(resourcePath); const std::string friendlyPath = PathParser::GetFriendlyPath(resourcePath); const uint32_t iconID = EDITOR_CONTEXT(editorResources)->GetFileIcon(path)->GetTexture().GetID(); @@ -61,11 +66,12 @@ void OvEditor::Helpers::PickerHelpers::AddFileItems( PathParser::EFileType p_fileType, std::function p_onSelected, bool p_searchProject, - bool p_searchEngine) + bool p_searchEngine, + std::function p_filter) { if (p_searchProject) - CollectFromDirectory(p_list, EDITOR_CONTEXT(projectAssetsPath), false, p_fileType, p_onSelected); + CollectFromDirectory(p_list, EDITOR_CONTEXT(projectAssetsPath), false, p_fileType, p_onSelected, p_filter); if (p_searchEngine) - CollectFromDirectory(p_list, EDITOR_CONTEXT(engineAssetsPath), true, p_fileType, p_onSelected); + CollectFromDirectory(p_list, EDITOR_CONTEXT(engineAssetsPath), true, p_fileType, p_onSelected, p_filter); } diff --git a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index 42036648f..1ca8eadd8 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -250,6 +250,7 @@ void OvEditor::Panels::Inspector::_DrawAddSection() return; const uint32_t componentIconID = EDITOR_CONTEXT(editorResources)->GetTexture("Component")->GetTexture().GetID(); + const uint32_t scriptIconID = EDITOR_CONTEXT(editorResources)->GetTexture("Script")->GetTexture().GetID(); OvCore::Helpers::GUIHelpers::PickerItemList items; @@ -307,9 +308,48 @@ void OvEditor::Panels::Inspector::_DrawAddSection() m_targetActor->AddBehaviour(scriptPath); }, - true, false + true, false, + [this](const std::string& p_resourcePath) { + const std::string scriptPath = EDITOR_EXEC(GetScriptPath(p_resourcePath)); + return !m_targetActor->GetBehaviour(scriptPath); + } ); + items.Add({ + "Create Script...", + "Create Script...", + "Create Script...", + scriptIconID, + [this] { + if (!m_targetActor.has_value()) + return; + + const std::string searchText = OvCore::Helpers::GUIHelpers::GetPickerSearchText(); + const std::string initialDir = EDITOR_CONTEXT(projectAssetsPath).string(); + const std::string destination = EDITOR_EXEC(CreateScript(initialDir, searchText)); + + if (destination.empty()) + return; + + const std::string scriptPath = EDITOR_EXEC(GetScriptPath(destination)); + const std::string displayName = OvTools::Utils::PathParser::GetElementName(scriptPath); + + if (m_targetActor->GetBehaviour(scriptPath)) + { + OvWindowing::Dialogs::MessageBox( + "Script already attached", + "The script \"" + displayName + "\" is already attached to this actor.", + OvWindowing::Dialogs::MessageBox::EMessageType::ERROR, + OvWindowing::Dialogs::MessageBox::EButtonLayout::OK + ); + return; + } + + m_targetActor->AddBehaviour(scriptPath); + }, + true /* alwaysVisible */ + }); + OvCore::Helpers::GUIHelpers::OpenPicker(std::move(items), "Add Component"); }; } diff --git a/Sources/OvEditor/src/OvEditor/Panels/ItemPicker.cpp b/Sources/OvEditor/src/OvEditor/Panels/ItemPicker.cpp index 3912ce028..ecaad9c37 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/ItemPicker.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/ItemPicker.cpp @@ -84,7 +84,7 @@ void ItemPicker::_Draw_Impl() const auto& items = m_items.Items(); for (size_t i = 0; i < m_rows.size() && i < items.size(); ++i) { - if (m_rows[i].second->enabled) + if (m_rows[i].widget->enabled) { items[i].onSelected(); Close(); @@ -164,12 +164,17 @@ void ItemPicker::Populate() Close(); }; - m_rows.emplace_back(item.key, &row); + m_rows.emplace_back(item.key, item.alwaysVisible, &row); } } void ItemPicker::FilterList(const std::string& p_search) { - for (auto& [key, row] : m_rows) - row->enabled = ContainsCaseInsensitive(key, p_search); + for (auto& entry : m_rows) + entry.widget->enabled = entry.alwaysVisible || ContainsCaseInsensitive(entry.key, p_search); +} + +std::string ItemPicker::GetSearchText() const +{ + return m_searchField ? m_searchField->content : ""; } diff --git a/Sources/OvWindowing/src/OvWindowing/Dialogs/FileDialog.cpp b/Sources/OvWindowing/src/OvWindowing/Dialogs/FileDialog.cpp index 361ac43f5..093b968e9 100644 --- a/Sources/OvWindowing/src/OvWindowing/Dialogs/FileDialog.cpp +++ b/Sources/OvWindowing/src/OvWindowing/Dialogs/FileDialog.cpp @@ -40,9 +40,6 @@ void OvWindowing::Dialogs::FileDialog::Show(EExplorerFlags p_flags) #ifdef _WIN32 OPENFILENAME ofn; - if (!m_initialDirectory.empty()) - m_filepath = m_initialDirectory; - if (!m_initialFilename.empty()) m_filepath = (std::filesystem::path{ m_initialDirectory } / m_initialFilename).string(); diff --git a/Sources/OvWindowing/src/OvWindowing/Dialogs/MessageBox.cpp b/Sources/OvWindowing/src/OvWindowing/Dialogs/MessageBox.cpp index cfb092d41..9d73bcf29 100644 --- a/Sources/OvWindowing/src/OvWindowing/Dialogs/MessageBox.cpp +++ b/Sources/OvWindowing/src/OvWindowing/Dialogs/MessageBox.cpp @@ -53,57 +53,72 @@ void OvWindowing::Dialogs::MessageBox::Spawn() if (glfwWin) { unsigned long xid = (unsigned long)glfwGetX11Window(glfwWin); - attachArg = " --attach=" + std::to_string(xid); + if (xid != 0) + attachArg = " --attach=" + std::to_string(xid); + } + + // For single-button (OK only) layouts, use the typed dialog (--info/--warning/--error). + // These don't support extra button labels but always show a single OK button. + // For multi-button layouts, use --question which supports ok-label/cancel-label. + const bool singleButton = + m_buttonLayout == EButtonLayout::OK || + m_buttonLayout == EButtonLayout::HELP; + + std::string dialogType; + if (singleButton) + { + switch (m_messageType) + { + case EMessageType::ERROR: dialogType = "--error"; break; + case EMessageType::WARNING: dialogType = "--warning"; break; + case EMessageType::INFORMATION: dialogType = "--info"; break; + default: dialogType = "--info"; break; + } } else { - printf("NO GLFW WINDOW\n"); + dialogType = "--question"; } - std::string command = "zenity --question"; // Always use question dialog for flexibility - command += attachArg; // <-- attach to our window - - // Add title and message + std::string command = "zenity " + dialogType; + command += attachArg; command += " --title=\"" + m_title + "\""; command += " --text=\"" + m_message + "\""; - command += " --no-markup"; // Prevent markup interpretation issues - - // Handle button layout + command += " --no-markup"; + bool useExtraButtons = false; - - switch (m_buttonLayout) + + if (!singleButton) { - case EButtonLayout::OK: - command += " --ok-label=\"OK\" --no-cancel"; - break; - case EButtonLayout::OK_CANCEL: - command += " --ok-label=\"OK\" --cancel-label=\"Cancel\""; - break; - case EButtonLayout::YES_NO: - command += " --ok-label=\"Yes\" --cancel-label=\"No\""; - break; - case EButtonLayout::YES_NO_CANCEL: - useExtraButtons = true; - // When using extra buttons, don't use ok-label or cancel-label - command += " --extra-button=\"Yes\" --extra-button=\"No\" --extra-button=\"Cancel\""; - break; - case EButtonLayout::RETRY_CANCEL: - command += " --ok-label=\"Retry\" --cancel-label=\"Cancel\""; - break; - case EButtonLayout::ABORT_RETRY_IGNORE: - useExtraButtons = true; - command += " --extra-button=\"Abort\" --extra-button=\"Retry\" --extra-button=\"Ignore\""; - break; - case EButtonLayout::CANCEL_TRYAGAIN_CONTINUE: - useExtraButtons = true; - command += " --extra-button=\"Cancel\" --extra-button=\"Try Again\" --extra-button=\"Continue\""; - break; - case EButtonLayout::HELP: - command += " --ok-label=\"Help\" --no-cancel"; - break; + switch (m_buttonLayout) + { + case EButtonLayout::OK_CANCEL: + command += " --ok-label=\"OK\" --cancel-label=\"Cancel\""; + break; + case EButtonLayout::YES_NO: + command += " --ok-label=\"Yes\" --cancel-label=\"No\""; + break; + case EButtonLayout::YES_NO_CANCEL: + useExtraButtons = true; + command += " --extra-button=\"Yes\" --extra-button=\"No\" --extra-button=\"Cancel\""; + break; + case EButtonLayout::RETRY_CANCEL: + command += " --ok-label=\"Retry\" --cancel-label=\"Cancel\""; + break; + case EButtonLayout::ABORT_RETRY_IGNORE: + useExtraButtons = true; + command += " --extra-button=\"Abort\" --extra-button=\"Retry\" --extra-button=\"Ignore\""; + break; + case EButtonLayout::CANCEL_TRYAGAIN_CONTINUE: + useExtraButtons = true; + command += " --extra-button=\"Cancel\" --extra-button=\"Try Again\" --extra-button=\"Continue\""; + break; + default: + break; + } } - - command += " 2>/dev/null"; // Suppress GTK warnings + + command += " 2>/dev/null"; // Execute zenity and check exit code int exitCode = system(command.c_str());