diff --git a/source/MaterialXGraphEditor/CMakeLists.txt b/source/MaterialXGraphEditor/CMakeLists.txt index 15547de47d..1785540bd8 100644 --- a/source/MaterialXGraphEditor/CMakeLists.txt +++ b/source/MaterialXGraphEditor/CMakeLists.txt @@ -19,6 +19,11 @@ endif() file(GLOB materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") file(GLOB materialx_headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h*") +if (APPLE) + list(APPEND materialx_source ${CMAKE_CURRENT_SOURCE_DIR}/FileDialog.mm) + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/FileDialog.mm" PROPERTIES LANGUAGE CXX) +endif () + file(GLOB imgui_source "${DEAR_IMGUI_PREFIX}/*.cpp") file(GLOB imgui_headers "${DEAR_IMGUI_PREFIX}/*.h*") @@ -49,7 +54,6 @@ include_directories("${DEAR_IMGUI_PREFIX}/backends") include_directories("${DEAR_IMGUI_PREFIX}/misc/cpp") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/External/ImGuiNodeEditor") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/External/ImGuiNodeEditor/examples/blueprints-example/utilities") -include_directories("${CMAKE_CURRENT_SOURCE_DIR}/External/ImGuiFileBrowser") set(GLFW_BUILD_EXAMPLES OFF) set(GLFW_BUILD_TESTS OFF) @@ -75,6 +79,11 @@ set(MATERIALX_LIBRARIES MaterialXGenGlsl MaterialXRenderGlsl) +if (APPLE) + find_library(CORE_FOUNDATION Foundation REQUIRED) + list(APPEND MATERIALX_LIBRARIES ${CORE_FOUNDATION}) +endif () + target_link_libraries( MaterialXGraphEditor PRIVATE diff --git a/source/MaterialXGraphEditor/FileDialog.cpp b/source/MaterialXGraphEditor/FileDialog.cpp new file mode 100644 index 0000000000..263c5706b4 --- /dev/null +++ b/source/MaterialXGraphEditor/FileDialog.cpp @@ -0,0 +1,244 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include "FileDialog.h" + +#include + +#if defined(_WIN32) + #include +#endif + +FileDialog::FileDialog(int flags) : + _flags(flags) +{ +} + +void FileDialog::setTitle(const std::string& title) +{ + _title = title; +} + +void FileDialog::setTypeFilters(const mx::StringVec& typeFilters) +{ + _filetypes.clear(); + + for (auto typefilter : typeFilters) + { + std::string minus_ext = typefilter.substr(1, typefilter.size() - 1); + std::pair filterPair = { minus_ext, minus_ext }; + _filetypes.push_back(filterPair); + } +} + +void FileDialog::open() +{ + clearSelected(); + _openFlag = true; +} + +bool FileDialog::isOpened() +{ + return _isOpened; +} + +bool FileDialog::hasSelected() +{ + return !_selectedFilenames.empty(); +} + +mx::FilePath FileDialog::getSelected() +{ + if (_selectedFilenames.empty()) + { + return {}; + } + + return *_selectedFilenames.begin(); +} + +void FileDialog::clearSelected() +{ + _selectedFilenames.clear(); +} + +void FileDialog::display() +{ + // Only call the dialog if it's not already displayed + if (!_openFlag || _isOpened) + { + return; + } + _openFlag = false; + + // Check if we want to save or open + bool save = !(_flags & FileDialogFlags_SelectDirectory) && + (_flags & FileDialogFlags_EnterNewFilename); + + std::string path = launchFileDialog(_filetypes, save); + if (!path.empty()) + { + _selectedFilenames.push_back(path); + } + + _isOpened = false; +} + +std::string launchFileDialog(const std::vector>& filetypes, bool save) +{ + mx::StringVec result = launchFileDialog(filetypes, save, false); + return result.empty() ? "" : result.front(); +} + +#if !defined(__APPLE__) +mx::StringVec launchFileDialog(const std::vector>& filetypes, bool save, bool multiple) +{ + static const int FILE_DIALOG_MAX_BUFFER = 16384; + if (save && multiple) + { + throw mx::Exception("save and multiple must not both be true."); + } + + #if defined(_WIN32) + OPENFILENAME ofn; + ZeroMemory(&ofn, sizeof(OPENFILENAME)); + ofn.lStructSize = sizeof(OPENFILENAME); + char tmp[FILE_DIALOG_MAX_BUFFER]; + ofn.lpstrFile = tmp; + ZeroMemory(tmp, FILE_DIALOG_MAX_BUFFER); + ofn.nMaxFile = FILE_DIALOG_MAX_BUFFER; + ofn.nFilterIndex = 1; + + std::string filter; + + if (!save && filetypes.size() > 1) + { + filter.append("Supported file types ("); + for (size_t i = 0; i < filetypes.size(); ++i) + { + filter.append("*."); + filter.append(filetypes[i].first); + if (i + 1 < filetypes.size()) + filter.append(";"); + } + filter.append(")"); + filter.push_back('\0'); + for (size_t i = 0; i < filetypes.size(); ++i) + { + filter.append("*."); + filter.append(filetypes[i].first); + if (i + 1 < filetypes.size()) + filter.append(";"); + } + filter.push_back('\0'); + } + for (auto pair : filetypes) + { + filter.append(pair.second); + filter.append(" (*."); + filter.append(pair.first); + filter.append(")"); + filter.push_back('\0'); + filter.append("*."); + filter.append(pair.first); + filter.push_back('\0'); + } + filter.push_back('\0'); + ofn.lpstrFilter = filter.data(); + + if (save) + { + ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT; + if (GetSaveFileNameA(&ofn) == FALSE) + return {}; + } + else + { + ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; + if (multiple) + ofn.Flags |= OFN_ALLOWMULTISELECT; + if (GetOpenFileNameA(&ofn) == FALSE) + return {}; + } + + size_t i = 0; + mx::StringVec result; + while (tmp[i] != '\0') + { + result.emplace_back(&tmp[i]); + i += result.back().size() + 1; + } + + if (result.size() > 1) + { + for (i = 1; i < result.size(); ++i) + { + result[i] = result[0] + "\\" + result[i]; + } + result.erase(begin(result)); + } + + if (save && ofn.nFilterIndex > 0) + { + auto ext = filetypes[ofn.nFilterIndex - 1].first; + if (ext != "*") + { + ext.insert(0, "."); + + auto& name = result.front(); + if (name.size() <= ext.size() || + name.compare(name.size() - ext.size(), ext.size(), ext) != 0) + { + name.append(ext); + } + } + } + + return result; + #else + char buffer[FILE_DIALOG_MAX_BUFFER]; + buffer[0] = '\0'; + + std::string cmd = "zenity --file-selection "; + // The safest separator for multiple selected paths is /, since / can never occur + // in file names. Only where two paths are concatenated will there be two / following + // each other. + if (multiple) + cmd += "--multiple --separator=\"/\" "; + if (save) + cmd += "--save "; + cmd += "--file-filter=\""; + for (auto pair : filetypes) + cmd += "\"*." + pair.first + "\" "; + cmd += "\""; + FILE* output = popen(cmd.c_str(), "r"); + if (output == nullptr) + throw mx::Exception("popen() failed -- could not launch zenity!"); + while (fgets(buffer, FILE_DIALOG_MAX_BUFFER, output) != NULL) + ; + pclose(output); + std::string paths(buffer); + paths.erase(std::remove(paths.begin(), paths.end(), '\n'), paths.end()); + + mx::StringVec result; + while (!paths.empty()) + { + size_t end = paths.find("//"); + if (end == std::string::npos) + { + result.emplace_back(paths); + paths = ""; + } + else + { + result.emplace_back(paths.substr(0, end)); + paths = paths.substr(end + 1); + } + } + + return result; + #endif +} +#endif diff --git a/source/MaterialXGraphEditor/FileDialog.h b/source/MaterialXGraphEditor/FileDialog.h new file mode 100644 index 0000000000..dd8750e53b --- /dev/null +++ b/source/MaterialXGraphEditor/FileDialog.h @@ -0,0 +1,51 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_FILEDIALOG_H +#define MATERIALX_FILEDIALOG_H + +#include + +namespace mx = MaterialX; + +enum FileDialogFlags +{ + FileDialogFlags_SelectDirectory = 1 << 0, // select directory instead of regular file + FileDialogFlags_EnterNewFilename = 1 << 1, // allow user to enter new filename when selecting regular file + FileDialogFlags_NoModal = 1 << 2, // file browsing window is modal by default. specify this to use a popup window + FileDialogFlags_NoTitleBar = 1 << 3, // hide window title bar + FileDialogFlags_NoStatusBar = 1 << 4, // hide status bar at the bottom of browsing window + FileDialogFlags_CloseOnEsc = 1 << 5, // close file browser when pressing 'ESC' + FileDialogFlags_CreateNewDir = 1 << 6, // allow user to create new directory + FileDialogFlags_MultipleSelection = 1 << 7, // allow user to select multiple files. this will hide FileDialogFlags_EnterNewFilename +}; + +// A native file browser class, based on the implementation in NanoGUI. +class FileDialog +{ + public: + FileDialog(int flags = 0); + void setTitle(const std::string& title); + void setTypeFilters(const mx::StringVec& typeFilters); + void open(); + bool isOpened(); + void display(); + bool hasSelected(); + mx::FilePath getSelected(); + void clearSelected(); + + private: + int _flags; + std::string _title; + bool _openFlag = false; + bool _isOpened = false; + std::vector _selectedFilenames; + std::vector> _filetypes; +}; + +std::string launchFileDialog(const std::vector>& filetypes, bool save); +mx::StringVec launchFileDialog(const std::vector>& filetypes, bool save, bool multiple); + +#endif diff --git a/source/MaterialXGraphEditor/FileDialog.mm b/source/MaterialXGraphEditor/FileDialog.mm new file mode 100644 index 0000000000..5e2af40d5b --- /dev/null +++ b/source/MaterialXGraphEditor/FileDialog.mm @@ -0,0 +1,55 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include "FileDialog.h" + +#include + +#import + +mx::StringVec launchFileDialog(const std::vector>& filetypes, bool save, bool multiple) +{ + if (save && multiple) + { + throw mx::Exception("launchFileDialog(): 'save' and 'multiple' must not both be true."); + } + + mx::StringVec result; + if (save) + { + NSSavePanel* saveDlg = [NSSavePanel savePanel]; + + NSMutableArray* types = [NSMutableArray new]; + for (size_t idx = 0; idx < filetypes.size(); ++idx) + [types addObject:[NSString stringWithUTF8String:filetypes[idx].first.c_str()]]; + + [saveDlg setAllowedFileTypes:types]; + + if ([saveDlg runModal] == NSModalResponseOK) + result.emplace_back([[[saveDlg URL] path] UTF8String]); + } + else + { + NSOpenPanel* openDlg = [NSOpenPanel openPanel]; + + [openDlg setCanChooseFiles:YES]; + [openDlg setCanChooseDirectories:NO]; + [openDlg setAllowsMultipleSelection:multiple]; + NSMutableArray* types = [NSMutableArray new]; + for (size_t idx = 0; idx < filetypes.size(); ++idx) + [types addObject:[NSString stringWithUTF8String:filetypes[idx].first.c_str()]]; + + [openDlg setAllowedFileTypes:types]; + + if ([openDlg runModal] == NSModalResponseOK) + { + for (NSURL* url in [openDlg URLs]) + { + result.emplace_back((char*) [[url path] UTF8String]); + } + } + } + return result; +} diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index fba8958c11..dddd9d0d54 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -47,7 +47,7 @@ Graph::Graph(const std::string& materialFilename, _libraryFolders(libraryFolders), _initial(false), _delete(false), - _fileDialogSave(ImGuiFileBrowserFlags_EnterNewFilename | ImGuiFileBrowserFlags_CreateNewDir), + _fileDialogSave(FileDialogFlags_EnterNewFilename | FileDialogFlags_CreateNewDir), _isNodeGraph(false), _graphTotalSize(0), _popup(false), @@ -1018,9 +1018,9 @@ void Graph::setConstant(UiNodePtr node, mx::InputPtr& input, const mx::UIPropert // browser button to select new file if (ImGui::Button("Browse")) { - _fileDialogImage.SetTitle("Node Input Dialog"); - _fileDialogImage.Open(); - _fileDialogImage.SetTypeFilters(_imageFilter); + _fileDialogImage.setTitle("Node Input Dialog"); + _fileDialogImage.open(); + _fileDialogImage.setTypeFilters(_imageFilter); } ImGui::SameLine(); ImGui::PushItemWidth(labelWidth); @@ -1030,15 +1030,15 @@ void Graph::setConstant(UiNodePtr node, mx::InputPtr& input, const mx::UIPropert ImGui::PopStyleColor(); // create and load document from selected file - if (_fileDialogImage.HasSelected()) + if (_fileDialogImage.hasSelected()) { // set the new filename to the complete file path - mx::FilePath fileName = mx::FilePath(_fileDialogImage.GetSelected().string()); + mx::FilePath fileName = _fileDialogImage.getSelected(); temp = fileName; // need to set the file prefix for the input to "" so that it can find the new file input->setAttribute(input->FILE_PREFIX_ATTRIBUTE, ""); - _fileDialogImage.ClearSelected(); - _fileDialogImage.SetTypeFilters(std::vector()); + _fileDialogImage.clearSelected(); + _fileDialogImage.setTypeFilters(std::vector()); } // set input value and update materials if different from previous value @@ -2884,23 +2884,23 @@ void Graph::loadGraphFromFile() _currUiNode = nullptr; } - _fileDialog.SetTitle("Open File"); - _fileDialog.SetTypeFilters(_mtlxFilter); - _fileDialog.Open(); + _fileDialog.setTitle("Open File"); + _fileDialog.setTypeFilters(_mtlxFilter); + _fileDialog.open(); } void Graph::saveGraphToFile() { - _fileDialogSave.SetTypeFilters(_mtlxFilter); - _fileDialogSave.SetTitle("Save File As"); - _fileDialogSave.Open(); + _fileDialogSave.setTypeFilters(_mtlxFilter); + _fileDialogSave.setTitle("Save File As"); + _fileDialogSave.open(); } void Graph::loadGeometry() { - _fileDialogGeom.SetTitle("Load Geometry"); - _fileDialogGeom.SetTypeFilters(_geomFilter); - _fileDialogGeom.Open(); + _fileDialogGeom.setTitle("Load Geometry"); + _fileDialogGeom.setTypeFilters(_geomFilter); + _fileDialogGeom.open(); } void Graph::graphButtons() @@ -2962,7 +2962,7 @@ void Graph::graphButtons() // Menu keys ImGuiIO& guiIO = ImGui::GetIO(); - if (guiIO.KeyCtrl && !_fileDialogSave.IsOpened() && !_fileDialog.IsOpened() && !_fileDialogGeom.IsOpened()) + if (guiIO.KeyCtrl && !_fileDialogSave.isOpened() && !_fileDialog.isOpened() && !_fileDialogGeom.isOpened()) { if (ImGui::IsKeyReleased(ImGuiKey_O)) { @@ -3551,7 +3551,7 @@ void Graph::handleRenderViewInputs(ImVec2 minValue, float width, float height) _renderer->setKeyEvent(ImGuiKey_KeypadSubtract); } // scrolling not possible if open or save file dialog is open - if (scrollAmt != 0 && !_fileDialogSave.IsOpened() && !_fileDialog.IsOpened() && !_fileDialogGeom.IsOpened()) + if (scrollAmt != 0 && !_fileDialogSave.isOpened() && !_fileDialog.isOpened() && !_fileDialogGeom.isOpened()) { _renderer->setScrollEvent(scrollAmt); } @@ -3795,13 +3795,13 @@ void Graph::drawGraph(ImVec2 mousePos) } // hotkey to frame selected node(s) - if (ImGui::IsKeyReleased(ImGuiKey_F) && !_fileDialogSave.IsOpened()) + if (ImGui::IsKeyReleased(ImGuiKey_F) && !_fileDialogSave.isOpened()) { ed::NavigateToSelection(); } // go back up from inside a subgraph - if (ImGui::IsKeyReleased(ImGuiKey_U) && (!ImGui::IsPopupOpen("add node")) && (!ImGui::IsPopupOpen("search")) && !_fileDialogSave.IsOpened()) + if (ImGui::IsKeyReleased(ImGuiKey_U) && (!ImGui::IsPopupOpen("add node")) && (!ImGui::IsPopupOpen("search")) && !_fileDialogSave.isOpened()) { upNodeGraph(); } @@ -3929,9 +3929,9 @@ void Graph::drawGraph(ImVec2 mousePos) } ed::Suspend(); - _fileDialogSave.Display(); + _fileDialogSave.display(); // saving file - if (_fileDialogSave.HasSelected()) + if (_fileDialogSave.hasSelected()) { std::string message; @@ -3940,13 +3940,13 @@ void Graph::drawGraph(ImVec2 mousePos) std::cerr << "*** Validation warnings for " << _materialFilename.getBaseName() << " ***" << std::endl; std::cerr << message; } - std::string fileName = _fileDialogSave.GetSelected().string(); - mx::FilePath name = _fileDialogSave.GetSelected().string(); + std::string fileName = _fileDialogSave.getSelected(); + mx::FilePath name = _fileDialogSave.getSelected(); ed::Resume(); savePosition(); writeText(fileName, name); - _fileDialogSave.ClearSelected(); + _fileDialogSave.clearSelected(); } else { @@ -3956,11 +3956,11 @@ void Graph::drawGraph(ImVec2 mousePos) ed::End(); ImGui::End(); - _fileDialog.Display(); + _fileDialog.display(); // create and load document from selected file - if (_fileDialog.HasSelected()) + if (_fileDialog.hasSelected()) { - mx::FilePath fileName = mx::FilePath(_fileDialog.GetSelected().string()); + mx::FilePath fileName = _fileDialog.getSelected(); _currGraphName.clear(); std::string graphName = fileName.getBaseName(); _currGraphName.push_back(graphName.substr(0, graphName.length() - 5)); @@ -3971,22 +3971,22 @@ void Graph::drawGraph(ImVec2 mousePos) buildUiBaseGraph(_graphDoc); _currGraphElem = _graphDoc; _prevUiNode = nullptr; - _fileDialog.ClearSelected(); + _fileDialog.clearSelected(); _renderer->setDocument(_graphDoc); _renderer->updateMaterials(nullptr); } - _fileDialogGeom.Display(); - if (_fileDialogGeom.HasSelected()) + _fileDialogGeom.display(); + if (_fileDialogGeom.hasSelected()) { - mx::FilePath fileName = mx::FilePath(_fileDialogGeom.GetSelected().string()); - _fileDialogGeom.ClearSelected(); + mx::FilePath fileName = _fileDialogGeom.getSelected(); + _fileDialogGeom.clearSelected(); _renderer->loadMesh(fileName); _renderer->updateMaterials(nullptr); } - _fileDialogImage.Display(); + _fileDialogImage.display(); } // return node location in graphNodes vector based off of node id diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 2a27b5702f..bb56a06ab0 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -6,13 +6,11 @@ #ifndef MATERIALX_GRAPH_H #define MATERIALX_GRAPH_H +#include #include #include -#include - #include -#include #include @@ -212,10 +210,11 @@ class Graph bool _delete; // file dialog information - ImGui::FileBrowser _fileDialog; - ImGui::FileBrowser _fileDialogSave; - ImGui::FileBrowser _fileDialogImage; - ImGui::FileBrowser _fileDialogGeom; + FileDialog _fileDialog; + FileDialog _fileDialogSave; + FileDialog _fileDialogImage; + FileDialog _fileDialogGeom; + bool _isNodeGraph;