Skip to content

Commit

Permalink
Added Native File Browser for Graph Editor (#1256)
Browse files Browse the repository at this point in the history
This PR adds support for using a platform native file browser to the MaterialXGraphEditor.
  • Loading branch information
dgovil authored Apr 3, 2023
1 parent 2d9c97e commit 612cb94
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 43 deletions.
11 changes: 10 additions & 1 deletion source/MaterialXGraphEditor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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*")

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
244 changes: 244 additions & 0 deletions source/MaterialXGraphEditor/FileDialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include "FileDialog.h"

#include <MaterialXCore/Exception.h>

#if defined(_WIN32)
#include <windows.h>
#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<std::string, std::string> 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<std::pair<std::string, std::string>>& filetypes, bool save)
{
mx::StringVec result = launchFileDialog(filetypes, save, false);
return result.empty() ? "" : result.front();
}

#if !defined(__APPLE__)
mx::StringVec launchFileDialog(const std::vector<std::pair<std::string, std::string>>& 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
51 changes: 51 additions & 0 deletions source/MaterialXGraphEditor/FileDialog.h
Original file line number Diff line number Diff line change
@@ -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 <MaterialXFormat/File.h>

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<mx::FilePath> _selectedFilenames;
std::vector<std::pair<std::string, std::string>> _filetypes;
};

std::string launchFileDialog(const std::vector<std::pair<std::string, std::string>>& filetypes, bool save);
mx::StringVec launchFileDialog(const std::vector<std::pair<std::string, std::string>>& filetypes, bool save, bool multiple);

#endif
55 changes: 55 additions & 0 deletions source/MaterialXGraphEditor/FileDialog.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include "FileDialog.h"

#include <MaterialXCore/Exception.h>

#import <Cocoa/Cocoa.h>

mx::StringVec launchFileDialog(const std::vector<std::pair<std::string, std::string>>& 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;
}
Loading

0 comments on commit 612cb94

Please sign in to comment.