From 83f4687198b56dd721eae1006a73f3df7a042443 Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Tue, 21 Apr 2026 15:28:39 +0200 Subject: [PATCH] Added save/open file dialogs --- extra/DefaultTheme.oss | 11 +-- other/TODO.txt | 12 +-- src/ogfx/gui/Window.cpp | 125 ++++++++++++++++++++++++++++++++ src/ogfx/gui/Window.hpp | 15 ++++ src/ogfx/gui/widgets/Button.cpp | 6 +- src/ogfx/gui/widgets/Button.hpp | 2 +- src/ostd/string/String.hpp | 1 + src/test/GuiTest.cpp | 12 ++- 8 files changed, 163 insertions(+), 21 deletions(-) diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index 23f9e26..3156614 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -160,17 +160,8 @@ window.backgroundColor = rgba(160, 160, 160, 255) useBackgroundGradient = true padding = rect(15, 8, 15, 8) margin = rect(0, 0, 0, 0) - showIcon = true + showIcon = false (icon) { - path = file("img.png") - animation = anim(frameCount: 36, \ - fps: 60, \ - columns: 9, \ - rows: 4, \ - frameWidth: 256, \ - frameHeight: 256) - size = vec2(32, 32) - animated = true } } (button:hover) { diff --git a/other/TODO.txt b/other/TODO.txt index 2d0729f..f651c63 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -41,21 +41,21 @@ Implement following widgets: ***Tab Panel ***Button ***Image/Icon + ***Save/Open File Dialogs (and folder dialogs) Radio Button Grouping - Text Input + Progressbar Horizontal Slider Vertical Slider ListBox ComboBox - TreeView - Progressbar + Tooltips Context Menu Menu Bar - Save/Open File Dialogs (and folder dialogs) - Hex Editor Layouts Vertical Horizontal Grid - Tooltips + Text Input + TreeView + Hex Editor diff --git a/src/ogfx/gui/Window.cpp b/src/ogfx/gui/Window.cpp index 5054d3a..2de6311 100644 --- a/src/ogfx/gui/Window.cpp +++ b/src/ogfx/gui/Window.cpp @@ -369,6 +369,105 @@ namespace ogfx __on_signal(signal); } + stdvec WindowCore::showOpenFileDialog(const stdvec& filterList, bool multiselect, const String& defaultPath) const + { + struct DialogResult { + stdvec paths; + bool completed { false }; + }; + DialogResult result; + + __store_file_dialog_filters(filterList); + + SDL_ShowOpenFileDialog( + [](void* userdata, const char* const* filelist, int filter) { + auto* res = static_cast(userdata); + if (filelist) { + for (const char* const* f = filelist; *f; f++) + res->paths.push_back(String(*f)); + } + res->completed = true; + }, + &result, + m_window, + m_sdlFilters.empty() ? nullptr : m_sdlFilters.data(), + m_sdlFilters.size(), + defaultPath.empty() ? nullptr : defaultPath.c_str(), + multiselect + ); + + while (!result.completed) { + SDL_PumpEvents(); + SDL_Delay(10); + } + + return result.paths; + } + + stdvec WindowCore::showOpenFolderDialog(bool multiselect, const String& defaultPath) const + { + struct DialogResult { + stdvec paths; + bool completed { false }; + }; + DialogResult result; + + SDL_ShowOpenFolderDialog( + [](void* userdata, const char* const* filelist, int filter) { + auto* res = static_cast(userdata); + if (filelist) { + for (const char* const* f = filelist; *f; f++) + res->paths.push_back(String(*f)); + } + res->completed = true; + }, + &result, + m_window, + defaultPath.empty() ? nullptr : defaultPath.c_str(), + multiselect + ); + + while (!result.completed) { + SDL_PumpEvents(); + SDL_Delay(10); + } + + return result.paths; + } + + String WindowCore::showSaveFileDialog(const stdvec& filterList, const String& defaultPath) const + { + struct DialogResult { + String path; + bool completed { false }; + }; + DialogResult result; + + __store_file_dialog_filters(filterList); + + SDL_ShowSaveFileDialog( + [](void* userdata, const char* const* filelist, int filter) { + auto* res = static_cast(userdata); + if (filelist && *filelist) { + res->path = String(*filelist); + } + res->completed = true; + }, + &result, + m_window, + m_sdlFilters.empty() ? nullptr : m_sdlFilters.data(), + m_sdlFilters.size(), + defaultPath.empty() ? nullptr : defaultPath.c_str() + ); + + while (!result.completed) { + SDL_PumpEvents(); + SDL_Delay(10); + } + + return result.path; + } + MouseEventData WindowCore::get_mouse_state(SDL_Event& event) { f32 mx = 0, my = 0, _mx = 0, _my = 0; @@ -637,6 +736,32 @@ namespace ogfx m_defaultStylesheetVariables["text_left"] = { String("").add(cast(eTextAlign::Left)), true }; } + void WindowCore::__store_file_dialog_filters(const stdvec& filters) const + { + m_patternStorage.clear(); + m_sdlFilters.clear(); + m_patternStorage.reserve(filters.size()); + m_sdlFilters.reserve(filters.size()); + + for (const auto& f : filters) + { + // Join extensions with semicolons: {"png", "jpg"} -> "png;jpg" + String pattern; + for (size_t i = 0; i < f.extensionList.size(); i++) + { + if (i > 0) pattern.add(";"); + pattern.add(f.extensionList[i]); + } + m_patternStorage.push_back(pattern); + + // SDL filter points at strings that must stay alive + m_sdlFilters.push_back({ + f.description.c_str(), + m_patternStorage.back().c_str() + }); + } + } + diff --git a/src/ogfx/gui/Window.hpp b/src/ogfx/gui/Window.hpp index d38ba88..a151020 100644 --- a/src/ogfx/gui/Window.hpp +++ b/src/ogfx/gui/Window.hpp @@ -34,6 +34,11 @@ namespace ogfx { class WindowCore : public ostd::BaseObject { + public: struct FileDialogFilter + { + String description { "" }; + stdvec extensionList; + }; public: enum class eCursor : u8 { Default = 0, @@ -79,6 +84,7 @@ namespace ogfx bool maximized { false }; bool alwaysOnTop { false }; }; + public: using FileDialogFilterList = stdvec; public: inline WindowCore(void) { } virtual ~WindowCore(void); @@ -99,7 +105,12 @@ namespace ogfx void setBlockingEventsRefreshFPS(u32 fps); void requestRedraw(void); void handleSignal(ostd::Signal& signal) override; + stdvec showOpenFileDialog(const FileDialogFilterList& filterList, bool multiselect, const String& defaultPath = "") const; + stdvec showOpenFolderDialog(bool multiselect, const String& defaultPath = "") const; + String showSaveFileDialog(const FileDialogFilterList& filterList, const String& defaultPath = "") const; + inline String showOpenFileDialog(const FileDialogFilterList& filterList) const { auto list = showOpenFileDialog(filterList, false); return (list.size() == 0 ? "" : list[0]); } + inline String showOpenFolderDialog(void) const { auto list = showOpenFolderDialog(false); return (list.size() == 0 ? "" : list[0]); } inline const ostd::Stylesheet* theme(void) const { return m_guiTheme; } inline void loadDefaultTHeme(void) { setTheme(DefaultTheme); } inline ostd::Stylesheet::VariableList& getDefaultStylesheetVariableList(void) { return m_defaultStylesheetVariables; } @@ -142,6 +153,7 @@ namespace ogfx private: void __handle_event(SDL_Event& event); void __load_default_stylesheet_variables(void); + void __store_file_dialog_filters(const FileDialogFilterList& filters) const; protected: SDL_Window* m_window { nullptr }; @@ -191,6 +203,9 @@ namespace ogfx SDL_Cursor* m_cursor_SW_Resize { nullptr }; SDL_Cursor* m_cursor_W_Resize { nullptr }; + mutable stdvec m_patternStorage; + mutable stdvec m_sdlFilters; + public: inline static constexpr i32 MaxBlockingEventsFPS { 240 }; inline static constexpr i32 DefaultBlockingEventsFPS { 30 }; diff --git a/src/ogfx/gui/widgets/Button.cpp b/src/ogfx/gui/widgets/Button.cpp index 263bbc1..270e76a 100644 --- a/src/ogfx/gui/widgets/Button.cpp +++ b/src/ogfx/gui/widgets/Button.cpp @@ -43,11 +43,10 @@ namespace ogfx void Button::onDraw(ogfx::BasicRenderer2D& gfx) { - // if (m_textChanged) + if (m_textChanged) __update_size(); if (isIconEnabled()) { - m_realIconSize = getIconSize().propy(getGlobalContentBounds().getSize().y); if (isAnimatedEnabled()) gfx.drawAnimation(m_anim, getGlobalContentPosition(), m_realIconSize); @@ -88,7 +87,7 @@ namespace ogfx String filePath = getThemeValue(theme, "icon.path", m_icon.getFilePath()); if (filePath != m_icon.getFilePath()) setIcon(filePath); - setIconSize(getThemeValue(theme, "icon.size", getSize())); + setIconSize(getThemeValue(theme, "icon.size", getIconSize())); if (isAnimatedEnabled()) { m_anim.create(m_animData); @@ -99,6 +98,7 @@ namespace ogfx void Button::__update_size(void) { + m_realIconSize = getIconSize().propy(getGlobalContentBounds().getSize().y); auto size = getWindow().getGFX().getStringDimensions(getText(), getFontSize()); size.x += getPadding().left(); size.x += getPadding().right(); diff --git a/src/ogfx/gui/widgets/Button.hpp b/src/ogfx/gui/widgets/Button.hpp index ec7e02e..3a9769d 100644 --- a/src/ogfx/gui/widgets/Button.hpp +++ b/src/ogfx/gui/widgets/Button.hpp @@ -41,9 +41,9 @@ namespace ogfx void setText(const String& text); void setIcon(const String& filePath); void applyTheme(const ostd::Stylesheet& theme) override; - OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated); inline String getText(void) const { return m_text; } inline Image& getIcon(void) { return m_icon; } + OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated); OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon); OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData); OSTD_PARAM_GETSET(Vec2, IconSize, m_iconSize); diff --git a/src/ostd/string/String.hpp b/src/ostd/string/String.hpp index edbfadb..5485ad2 100644 --- a/src/ostd/string/String.hpp +++ b/src/ostd/string/String.hpp @@ -84,6 +84,7 @@ namespace ostd inline String& clr(void) { m_data = ""; return *this; } inline String& set(const cpp_string& str) { m_data = str; return *this; } inline stdvec getUTF8Codepoints(void) const { return decodeUTF8(m_data); } + inline bool empty(void) const { return m_data.empty(); } inline auto begin(void) { return m_data.begin(); } inline auto end(void) { return m_data.end(); } diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index cacc011..51d00e5 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -23,6 +23,11 @@ #include #include +ogfx::WindowCore::FileDialogFilterList filters = { + { "Image files", { "png", "jpg", "jpeg", "bmp" } }, + { "All files", { "*" } } +}; + ostd::ConsoleOutputHandler out; using namespace ogfx::gui::widgets; @@ -69,8 +74,13 @@ class Window : public ogfx::gui::Window m_btn1.setText("BUTTON"); m_btn1.setCallback(ogfx::gui::Widget::eCallback::MousePressed, [&](const ogfx::gui::Event& event) -> void { - + std::cout << showOpenFileDialog(filters) << "\n"; }); + m_btn1.addThemeOverride("@.button.showIcon", true); + m_btn1.setAnimationData(ad); + m_btn1.setIcon("./img.png"); + m_btn1.setIconSize({ 64, 64 }); + m_btn1.enableAnimated(); m_label2.setText("Label2"); m_label3.setText("Label3");