From 1603402df22796f2f24517da61b3e82f7e7f5e2e Mon Sep 17 00:00:00 2001 From: verylowfreq <60875431+verylowfreq@users.noreply.github.com> Date: Sat, 13 Aug 2022 21:10:44 +0900 Subject: [PATCH] Web: Improve file dialog. --- src/CMakeLists.txt | 10 +- src/platform/guihtml.cpp | 240 ++++++------- src/platform/html/emshell.html | 1 + src/platform/html/filemanagerui.js | 525 +++++++++++++++++++++++++++++ src/platform/html/solvespaceui.css | 23 ++ src/platform/html/solvespaceui.js | 5 +- src/solvespace.cpp | 19 ++ 7 files changed, 677 insertions(+), 146 deletions(-) create mode 100644 src/platform/html/filemanagerui.js diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f17c737..1f21e16a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -345,9 +345,15 @@ if(ENABLE_GUI) COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.js ${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js - COMMENT "Copying UI script" + COMMENT "Copying UI script solvespaceui.js" + VERBATIM) + add_custom_command( + TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/filemanagerui.js + ${EXECUTABLE_OUTPUT_PATH}/filemanagerui.js + COMMENT "Copying UI script filemanagerui.sj" VERBATIM) - else() target_sources(solvespace PRIVATE platform/guigtk.cpp) diff --git a/src/platform/guihtml.cpp b/src/platform/guihtml.cpp index fd3509d0..f5e48e18 100644 --- a/src/platform/guihtml.cpp +++ b/src/platform/guihtml.cpp @@ -1229,29 +1229,28 @@ public: auto it = std::remove(dialogsOnScreen.begin(), dialogsOnScreen.end(), shared_from_this()); dialogsOnScreen.erase(it); - }; - responseFuncs.push_back(responseFunc); - htmlButton.call("addEventListener", val("trigger"), Wrap(&responseFuncs.back())); - - static std::function updateShowFlagFunc = [this] { + this->is_shown = false; }; - htmlButton.call("addEventListener", val("trigger"), Wrap(&updateShowFlagFunc)); + if (responseFuncs.size() == 0) { + //FIXME(emscripten): I don't know why but the item in the head of responseFuncs cannot call. + // So add dummy item + responseFuncs.push_back([]{ }); + } + responseFuncs.push_back(responseFunc); + std::function* callback = &responseFuncs.back(); + htmlButton.call("addEventListener", val("trigger"), Wrap(callback)); htmlButtons.call("appendChild", htmlButton); } Response RunModal() { - // ssassert(false, "RunModal not supported on Emscripten"); - // dbp("MessageDialog::RunModal() called."); this->ShowModal(); //FIXME(emscripten): use val::await() with JavaScript's Promise while (true) { if (this->is_shown) { - // dbp("MessageDialog::RunModal(): is_shown == true"); - emscripten_sleep(2000); + emscripten_sleep(50); } else { - // dbp("MessageDialog::RunModal(): break due to is_shown == false"); break; } } @@ -1260,7 +1259,6 @@ public: return this->latestResponse; } else { // FIXME(emscripten): - // dbp("MessageDialog::RunModal(): Cannot get Response."); return this->latestResponse; } } @@ -1281,161 +1279,97 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { // File dialogs //----------------------------------------------------------------------------- -class FileOpenDialogImplHtml : public FileDialog { +// In emscripten psuedo filesystem, all userdata will be stored in this directory. +static std::string basePathInFilesystem = "/data/"; + + +/* FileDialog that can open, save and browse. Also refer `src/platform/html/filemanagerui.js`. + */ +class FileDialogImplHtml : public FileDialog { public: + + enum class Modes { + OPEN = 0, + SAVE, + BROWSER + }; + + Modes mode; + std::string title; std::string filename; std::string filters; - emscripten::val fileUploadHelper; + val jsFileManagerUI; - FileOpenDialogImplHtml() { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::FileOpenDialogImplHtml()"); - // FIXME(emscripten): workaround. following code raises "constructor is not a constructor" exception. - // val fuh = val::global("FileUploadHelper"); - // this->fileUploadHelper = fuh.new_(); - this->fileUploadHelper = val::global().call("createFileUploadHelperInstance"); - dbp("FileOpenDialogImplHtml::FileOpenDialogImplHtml() OK."); + FileDialogImplHtml(Modes mode) { + dbp("FileDialogImplHtml::FileDialogImplHtml()"); + val fileManagerUIClass = val::global("window")["FileManagerUI"]; + val dialogModeValue; + this->mode = mode; + if (mode == Modes::OPEN) { + dialogModeValue = val(0); + } else if (mode == Modes::SAVE) { + dialogModeValue = val(1); + } else { + dialogModeValue = val(2); + } + this->jsFileManagerUI = fileManagerUIClass.new_(dialogModeValue); + dbp("FileDialogImplHtml::FileDialogImplHtml() Done."); } - ~FileOpenDialogImplHtml() override { - dbp("FileOpenDialogImplHtml::~FileOpenDialogImplHtml()"); - this->fileUploadHelper.call("dispose"); + ~FileDialogImplHtml() override { + dbp("FileDialogImplHtml::~FileDialogImplHtml()"); + this->jsFileManagerUI.call("dispose"); } void SetTitle(std::string title) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::SetTitle(): title=%s", title.c_str()); + dbp("FileDialogImplHtml::SetTitle(): title=\"%s\"", title.c_str()); this->title = title; - //FIXME(emscripten): - this->fileUploadHelper.set("title", val(this->title)); + this->jsFileManagerUI.call("setTitle", val(title)); } void SetCurrentName(std::string name) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::SetCurrentName(): name=%s", name.c_str()); - SetFilename(GetFilename().Parent().Join(name)); + dbp("FileDialogImplHtml::SetCurrentName(): name=\"%s\", parent=\"%s\"", name.c_str(), this->GetFilename().Parent().raw.c_str()); + + Path filepath = Path::From(name); + if (filepath.IsAbsolute()) { + // dbp("FileDialogImplHtml::SetCurrentName(): path is absolute."); + SetFilename(filepath); + } else { + // dbp("FileDialogImplHtml::SetCurrentName(): path is relative."); + SetFilename(GetFilename().Parent().Join(name)); + } } Platform::Path GetFilename() override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::GetFilename()"); return Platform::Path::From(this->filename.c_str()); } void SetFilename(Platform::Path path) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::SetFilename(): path=%s", path.raw.c_str()); - this->filename = path.raw; - //FIXME(emscripten): - this->fileUploadHelper.set("filename", val(this->filename)); + dbp("FileDialogImplHtml::GetFilename(): path=\"%s\"", path.raw.c_str()); + this->filename = std::string(path.raw); + std::string filename_ = Path::From(this->filename).FileName(); + this->jsFileManagerUI.call("setDefaultFilename", val(filename_)); } void SuggestFilename(Platform::Path path) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::SuggestFilename(): path=%s", path.raw.c_str()); + dbp("FileDialogImplHtml::SuggestFilename(): path=\"%s\"", path.raw.c_str()); SetFilename(Platform::Path::From(path.FileStem())); } void AddFilter(std::string name, std::vector extensions) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::AddFilter()"); - this->filters = ""; - for (auto extension : extensions) { - this->filters = "." + extension; + if (this->filters.length() > 0) { this->filters += ","; } - dbp("filter=%s", this->filters.c_str()); - } - - void FreezeChoices(SettingsRef settings, const std::string &key) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::FreezeChoise()"); - } - - void ThawChoices(SettingsRef settings, const std::string &key) override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::ThawChoices()"); - } - - bool RunModal() override { - //FIXME(emscripten): - dbp("FileOpenDialogImplHtml::RunModal()"); - this->filename = "/untitled.slvs"; - this->fileUploadHelper.call("showDialog"); - - //FIXME(emscripten): use val::await() with JavaScript's Promise - dbp("FileOpenDialogImplHtml: start loop"); - // Wait until fileUploadHelper.is_shown == false - while (true) { - bool is_shown = this->fileUploadHelper["is_shown"].as(); - if (!is_shown) { - // dbp("FileOpenDialogImplHtml: break due to is_shown == false"); - break; - } else { - // dbp("FileOpenDialogImplHtml: sleep 100msec... (%d)", is_shown); - emscripten_sleep(100); - } - } - - val selectedFilenameVal = this->fileUploadHelper["currentFilename"]; - - if (selectedFilenameVal == val::null()) { - // dbp("selectedFilenameVal is null"); - return false; - } else { - std::string selectedFilename = selectedFilenameVal.as(); - this->filename = selectedFilename; - return true; - } - } -}; - -class FileSaveDummyDialogImplHtml : public FileDialog { -public: - std::string title; - std::string filename; - std::string filters; - - FileSaveDummyDialogImplHtml() { - - - } - - ~FileSaveDummyDialogImplHtml() override { - - } - - void SetTitle(std::string title) override { - this->title = title; - } - - void SetCurrentName(std::string name) override { - SetFilename(GetFilename().Parent().Join(name)); - } - - Platform::Path GetFilename() override { - return Platform::Path::From(this->filename.c_str()); - } - - void SetFilename(Platform::Path path) override { - this->filename = path.raw; - } - - void SuggestFilename(Platform::Path path) override { - SetFilename(Platform::Path::From(path.FileStem())); - } - - void AddFilter(std::string name, std::vector extensions) override { - this->filters = ""; for (size_t i = 0; i < extensions.size(); i++) { if (i != 0) { this->filters += ","; } - this->filters = "." + extensions[i]; + this->filters += "." + extensions[i]; } - dbp("filter=%s", this->filters.c_str()); + dbp("FileDialogImplHtml::AddFilter(): filter=%s", this->filters.c_str()); + this->jsFileManagerUI.call("setFilter", val(this->filters)); } void FreezeChoices(SettingsRef settings, const std::string &key) override { @@ -1443,27 +1377,49 @@ public: } void ThawChoices(SettingsRef settings, const std::string &key) override { - + //FIXME(emscripten): implement } bool RunModal() override { - if (this->filename.length() < 1) { - this->filename = "untitled.slvs"; + dbp("FileDialogImplHtml::RunModal()"); + + this->jsFileManagerUI.call("setBasePath", val(basePathInFilesystem)); + this->jsFileManagerUI.call("show"); + while (true) { + bool isShown = this->jsFileManagerUI.call("isShown"); + if (!isShown) { + break; + } else { + emscripten_sleep(50); + } + } + + dbp("FileSaveDialogImplHtml::RunModal() : dialog closed."); + + std::string selectedFilename = this->jsFileManagerUI.call("getSelectedFilename"); + if (selectedFilename.length() > 0) { + // Dummy call to set parent directory + this->SetFilename(Path::From(basePathInFilesystem + "/dummy")); + this->SetCurrentName(selectedFilename); + } + + + if (selectedFilename.length() > 0) { + return true; + } else { + return false; } - return true; } }; FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { - // FIXME(emscripten): implement - // dbp("CreateOpenFileDialog()"); - return std::shared_ptr(new FileOpenDialogImplHtml()); + dbp("CreateOpenFileDialog()"); + return std::shared_ptr(new FileDialogImplHtml(FileDialogImplHtml::Modes::OPEN)); } FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { - // FIXME(emscripten): implement - // dbp("CreateSaveFileDialog()"); - return std::shared_ptr(new FileSaveDummyDialogImplHtml()); + dbp("CreateSaveFileDialog()"); + return std::shared_ptr(new FileDialogImplHtml(FileDialogImplHtml::Modes::SAVE)); } //----------------------------------------------------------------------------- @@ -1481,7 +1437,7 @@ void OpenInBrowser(const std::string &url) { void OnSaveFinishedCallback(const Platform::Path& filename, bool is_saveAs, bool is_autosave) { dbp("OnSaveFinished(): %s, is_saveAs=%d, is_autosave=%d\n", filename.FileName().c_str(), is_saveAs, is_autosave); - std::string filename_str = filename.FileName(); + std::string filename_str = filename.raw; EM_ASM(saveFileDone(UTF8ToString($0), $1, $2), filename_str.c_str(), is_saveAs, is_autosave); } diff --git a/src/platform/html/emshell.html b/src/platform/html/emshell.html index 5de4354c..951d588e 100644 --- a/src/platform/html/emshell.html +++ b/src/platform/html/emshell.html @@ -6,6 +6,7 @@ -->
diff --git a/src/platform/html/filemanagerui.js b/src/platform/html/filemanagerui.js new file mode 100644 index 00000000..f82499b0 --- /dev/null +++ b/src/platform/html/filemanagerui.js @@ -0,0 +1,525 @@ +"use strict"; + +const FileManagerUI_OPEN = 0; +const FileManagerUI_SAVE = FileManagerUI_OPEN + 1; +const FileManagerUI_BROWSE = FileManagerUI_SAVE + 1; + +//FIXME(emscripten): File size thresholds. How large file can we accept safely ? + +/** Maximum filesize for a uploaded file. + * @type {number} */ +const FileManagerUI_UPLOAD_FILE_SIZE_LIMIT = 50 * 1000 * 1000; + +const tryMakeDirectory = (path) => { + try { + FS.mkdir(path); + } catch { + // NOP + } +} + + +class FileManagerUI { + /** + * @param {number} mode - dialog mode FileManagerUI_[ OPEN, SAVE, BROWSE ] + */ + constructor(mode) { + /** @type {boolean} */ + this.__isOpenDialog = false; + /** @type {boolean} */ + this.__isSaveDialog = false; + /** @type {boolean} */ + this.__isBrowseDialog = false; + + if (mode == FileManagerUI_OPEN) { + this.__isOpenDialog = true; + } else if (mode == FileManagerUI_SAVE) { + this.__isSaveDialog = true; + } else { + this.__isBrowseDialog = true; + } + + /** @type {boolean} true if the dialog is shown. */ + this.__isShown = false; + + /** @type {string[]} */ + this.__extension_filters = [".slvs"]; + + /** @type {string} */ + this.__basePathInFilesystem = ""; + + /** @type {string} filename user selected. empty if nothing selected */ + this.__selectedFilename = ""; + + this.__closedWithCancel = false; + + this.__defaultFilename = "untitled"; + } + + /** deconstructor + */ + dispose() { + if (this.__dialogRootElement) { + this.__dialogHeaderElement = null; + this.__descriptionElement = null; + this.__filelistElement = null; + this.__fileInputElement = null; + this.__saveFilenameInputElement = null; + this.__buttonContainerElement = null; + this.__dialogRootElement.parentElement.removeChild(this.__dialogRootElement); + this.__dialogRootElement = null; + } + } + + /** + * @param {string} label + * @param {string} response + * @param {bool} isDefault + */ + __addButton(label, response, isDefault, onclick) { + const buttonElem = document.createElement("div"); + addClass(buttonElem, "button"); + setLabelWithMnemonic(buttonElem, label); + if (isDefault) { + addClass(buttonElem, "default"); + addClass(buttonElem, "selected"); + } + buttonElem.addEventListener("click", () => { + if (onclick) { + if (onclick()) { + this.__close(); + } + } else { + this.__close(); + } + }); + + this.__buttonContainerElement.appendChild(buttonElem); + } + + /** + * @param {HTMLElement} div element that built + */ + buildDialog() { + const root = document.createElement('div'); + addClass(root, "modal"); + root.style.display = "none"; + root.style.zIndex = 1000; + + const dialog = document.createElement('div'); + addClass(dialog, "dialog"); + addClass(dialog, "wide"); + root.appendChild(dialog); + + const messageHeader = document.createElement('strong'); + this.__dialogHeaderElement = messageHeader; + addClass(messageHeader, "dialog_header"); + dialog.appendChild(messageHeader); + + const description = document.createElement('p'); + this.__descriptionElement = description; + dialog.appendChild(description); + + const filelistheader = document.createElement('h3'); + filelistheader.textContent = 'Files:'; + dialog.appendChild(filelistheader); + + const filelist = document.createElement('ul'); + this.__filelistElement = filelist; + addClass(filelist, 'filelist'); + dialog.appendChild(filelist); + + const dummyfilelistitem = document.createElement('li'); + dummyfilelistitem.textContent = "(No file in psuedo filesystem)"; + filelist.appendChild(dummyfilelistitem); + + if (this.__isOpenDialog) { + const fileuploadcontainer = document.createElement('div'); + dialog.appendChild(fileuploadcontainer); + + const fileuploadheader = document.createElement('h3'); + fileuploadheader.textContent = "Upload file:"; + fileuploadcontainer.appendChild(fileuploadheader); + + const dragdropdescription = document.createElement('p'); + dragdropdescription.textContent = "(Drag & drop file to the following box)"; + dragdropdescription.style.fontSize = "0.8em"; + dragdropdescription.style.margin = "0.1em"; + fileuploadcontainer.appendChild(dragdropdescription); + + const filedroparea = document.createElement('div'); + addClass(filedroparea, 'filedrop'); + filedroparea.addEventListener('dragstart', (ev) => this.__onFileDragDrop(ev)); + filedroparea.addEventListener('dragover', (ev) => this.__onFileDragDrop(ev)); + filedroparea.addEventListener('dragleave', (ev) => this.__onFileDragDrop(ev)); + filedroparea.addEventListener('drop', (ev) => this.__onFileDragDrop(ev)); + fileuploadcontainer.appendChild(filedroparea); + + const fileinput = document.createElement('input'); + this.__fileInputElement = fileinput; + fileinput.setAttribute('type', 'file'); + fileinput.style.width = "100%"; + fileinput.addEventListener('change', (ev) => this.__onFileInputChanged(ev)); + filedroparea.appendChild(fileinput); + + } else if (this.__isSaveDialog) { + const filenameinputcontainer = document.createElement('div'); + dialog.appendChild(filenameinputcontainer); + + const filenameinputheader = document.createElement('h3'); + filenameinputheader.textContent = "Filename:"; + filenameinputcontainer.appendChild(filenameinputheader); + + const filenameinput = document.createElement('input'); + filenameinput.setAttribute('type', 'input'); + filenameinput.style.width = "90%"; + filenameinput.style.margin = "auto 1em auto 1em"; + this.__saveFilenameInputElement = filenameinput; + filenameinputcontainer.appendChild(filenameinput); + } + + // Paragraph element for spacer + dialog.appendChild(document.createElement('p')); + + const buttoncontainer = document.createElement('div'); + this.__buttonContainerElement = buttoncontainer; + addClass(buttoncontainer, "buttons"); + dialog.appendChild(buttoncontainer); + + this.__addButton('OK', 0, false, () => { + if (this.__isOpenDialog) { + let selectedFilename = null; + const fileitems = document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]'); + Array.from(fileitems).forEach((radiobox) => { + if (radiobox.checked) { + selectedFilename = radiobox.parentElement.getAttribute('data-filename'); + } + }); + if (selectedFilename) { + return true; + } else { + return false; + } + } else { + return true; + } + }); + + this.__addButton('Cancel', 1, true, () => { + this.__closedWithCancel = true; + return true; + }); + + return root; + } + + /** + * @param {string} text + */ + setTitle(text) { + this.__dialogHeaderText = text; + } + + /** + * @param {string} text + */ + setDescription(text) { + this.__descriptionText = text; + } + + /** + * @param {string} path file prefix. (ex) 'tmp/' to '/tmp/filename.txt' + */ + setBasePath(path) { + this.__basePathInFilesystem = path; + tryMakeDirectory(path); + } + + /** + * @param {string} filename + */ + setDefaultFilename(filename) { + this.__defaultFilename = filename; + } + + /** + * + * @param {string} filter comma-separated extensions like ".slvs,.stl;." + */ + setFilter(filter) { + const exts = filter.split(','); + this.__extension_filters = exts; + } + + __buildFileEntry(filename) { + const lielem = document.createElement('li'); + const label = document.createElement('label'); + label.setAttribute('data-filename', filename); + lielem.appendChild(label); + const radiobox = document.createElement('input'); + radiobox.setAttribute('type', 'radio'); + if (!this.__isOpenDialog) { + radiobox.style.display = "none"; + } + radiobox.setAttribute('name', 'filemanager_filelist'); + label.appendChild(radiobox); + const filenametext = document.createTextNode(filename); + label.appendChild(filenametext); + + return lielem; + } + + /** + * @returns {string[]} filename array + */ + __getFileEntries() { + const basePath = this.__basePathInFilesystem; + /** @type {any[]} */ + const nodes = FS.readdir(basePath); + /** @type {string[]} */ + const files = nodes.filter((nodename) => { + return FS.isFile(FS.lstat(basePath + nodename).mode); + }); + /*.map((filename) => { + return basePath + filename; + });*/ + console.log(`__getFileEntries():`, files); + return files; + } + + /** + * @param {string[]?} files file list already constructed + * @returns {string[]} filename array + */ + __getFileEntries_recurse(basePath) { + //FIXME:remove try catch block + try { + //const basePath = this.__basePathInFilesystem; + FS.currentPath = basePath; + /** @type {any[]} */ + const nodes = FS.readdir(basePath); + + const filesInThisDirectory = nodes.filter((nodename) => { + return FS.isFile(FS.lstat(basePath + "/" + nodename).mode); + }).map((filename) => { + return basePath + "/" + filename; + }); + let files = filesInThisDirectory; + + const directories = nodes.filter((nodename) => { + return FS.isDir(FS.lstat(basePath + "/" + nodename).mode); + }); + + for (let i = 0; i < directories.length; i++) { + const directoryname = directories[i]; + if (directoryname == '.' || directoryname == '..') { + continue; + } + const orig_cwd = FS.currentPath; + const directoryfullpath = basePath + "/" + directoryname; + FS.currentPath = directoryfullpath; + files = files.concat(this.__getFileEntries_recurse(directoryfullpath)); + FS.currentPath = orig_cwd; + } + + console.log(`__getFileEntries_recurse(): in "${basePath}"`, files); + return files; + + } catch (excep) { + console.log(excep); + throw excep; + } + } + + __updateFileList() { + console.log(`__updateFileList()`); + Array.from(this.__filelistElement.children).forEach((elem) => { + this.__filelistElement.removeChild(elem); + }); + // const files = this.__getFileEntries(); + FS.currentPath = this.__basePathInFilesystem; + const files = this.__getFileEntries_recurse(this.__basePathInFilesystem); + if (files.length < 1) { + const dummyfilelistitem = document.createElement('li'); + dummyfilelistitem.textContent = "(No file in psuedo filesystem)"; + this.__filelistElement.appendChild(dummyfilelistitem); + + } else { + files.forEach((entry) => { + this.__filelistElement.appendChild(this.__buildFileEntry(entry)); + }); + } + } + + + /** + * @param {File} file + */ + __getFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { + const filereader = new FileReader(); + filereader.onerror = (ev) => { + reject(ev); + }; + filereader.onload = (ev) => { + resolve(ev.target.result); + }; + filereader.readAsArrayBuffer(file); + }); + } + + /** + * + * @param {File} file + */ + async __tryAddFile(file) { + return new Promise(async (resolve, reject) => { + if (!file) { + reject(new Error(`Invalid arg: file is ${file}`)); + + } else if (file.size > FileManagerUI_UPLOAD_FILE_SIZE_LIMIT) { + //FIXME(emscripten): Use our MessageDialog instead of browser's alert(). + alert(`Specified file is larger than limit of ${FileManagerUI_UPLOAD_FILE_SIZE_LIMIT} bytes. Canceced.`); + reject(new Error(`File is too large: "${file.name} is ${file.size} bytes`)); + + } else { + // Just add to Filesystem + const path = `${this.__basePathInFilesystem}${file.name}`; + const blobArrayBuffer = await this.__getFileAsArrayBuffer(file); + const u8array = new Uint8Array(blobArrayBuffer); + const fs = FS.open(path, "w"); + FS.write(fs, u8array, 0, u8array.length, 0); + FS.close(fs); + resolve(); + } + }); + } + + __addSelectedFile() { + if (this.__fileInputElement.files.length < 1) { + console.warn(`No file selected.`); + return; + } + + const file = this.__fileInputElement.files[0]; + this.__tryAddFile(file) + .then(() => { + this.__updateFileList(); + }) + .catch((err) => { + this.__fileInputElement.value = null; + console.error(err); + }) + } + + /** + * @param {DragEvent} ev + */ + __onFileDragDrop(ev) { + ev.preventDefault(); + if (ev.type == "dragenter" || ev.type == "dragover" || ev.type == "dragleave") { + return; + } + if (ev.dataTransfer.files.length < 1) { + return; + } + this.__fileInputElement.files = ev.dataTransfer.files; + + this.__addSelectedFile(); + } + + /** + * @param {InputEvent} _ev + */ + __onFileInputChanged(_ev) { + this.__addSelectedFile(); + } + + + /** Show the FileManager UI dialog */ + __show() { + this.__closedWithCancel = false; + + /** @type {HTMLElement} */ + this.__dialogRootElement = this.buildDialog(); + document.querySelector('body').appendChild(this.__dialogRootElement); + + this.__dialogHeaderElement.textContent = this.__dialogHeaderText || "File manager"; + this.__descriptionElement.textContent = this.__descriptionText || "Select a file."; + if (this.__extension_filters) { + this.__descriptionElement.textContent += "Requested filter is " + this.__extension_filters.join(", "); + } + + if (this.__isOpenDialog && this.__extension_filters) { + this.__fileInputElement.accept = this.__extension_filters.concat(','); + } + + if (this.__isSaveDialog) { + this.__saveFilenameInputElement.value = this.__defaultFilename; + } + + this.__dialogRootElement.style.display = "block"; + this.__isShown = true; + } + + /** Close the dialog */ + __close() { + this.__selectedFilename = ""; + if (this.__isOpenDialog) { + Array.from(document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]')) + .forEach((elem) => { + if (elem.checked) { + this.__selectedFilename = elem.parentElement.getAttribute("data-filename"); + } + }); + } else if (this.__isSaveDialog) { + if (!this.__closedWithCancel) { + this.__selectedFilename = this.__saveFilenameInputElement.value; + } + } + + Array.from(this.__filelistElement.children).forEach((elem) => { + this.__filelistElement.removeChild(elem); + }); + + this.dispose(); + + this.__isShown = false; + } + + /** + * @return {boolean} + */ + isShown() { + return this.__isShown; + } + + /** + * + * @returns {Promise} filename string on resolved. + */ + showModalAsync() { + return new Promise((resolve, reject) => { + this.__show(); + this.__updateFileList(); + const intervalTimer = setInterval(() => { + if (!this.isShown()) { + clearInterval(intervalTimer); + resolve(this.__selectedFilename); + } + }, 50); + }); + } + + getSelectedFilename() { + return this.__selectedFilename; + } + + show() { + this.__show(); + this.__updateFileList(); + } +}; + + +window.FileManagerUI = FileManagerUI; diff --git a/src/platform/html/solvespaceui.css b/src/platform/html/solvespaceui.css index 95f9e61f..3d2f44bb 100644 --- a/src/platform/html/solvespaceui.css +++ b/src/platform/html/solvespaceui.css @@ -246,11 +246,34 @@ main { min-width: 200px; max-width: 400px; white-space: pre-wrap; + max-height: 70%; + overflow-y: auto; +} +.dialog.wide { + width: 80%; + max-width: 1200px; } .dialog > .buttons { display: flex; justify-content: space-around; } +.dialog .filedrop { + margin: 1em 0 1em 0; + padding: 1em; + border: 2px solid black; + background-color: hsl(0, 0%, 50%); +} +.dialog .filelist { + display: flex; + flex-flow: row wrap; + list-style: none; + margin: 0; + padding: 0; +} +.dialog .filelist li { + padding: 0.2em 0.5em 0.2em 0.5em; + break-inside: avoid; +} /* Mnemonics */ .label > u { diff --git a/src/platform/html/solvespaceui.js b/src/platform/html/solvespaceui.js index fe870d15..edcca32e 100644 --- a/src/platform/html/solvespaceui.js +++ b/src/platform/html/solvespaceui.js @@ -657,8 +657,9 @@ class FileDownloadHelper { this.descriptionParagraph.innerHTML = ""; const linkElem = document.createElement("a"); - let downloadfilename = "solvespace_browser-"; - downloadfilename += `${GetCurrentDateTimeString()}.slvs`; + //let downloadfilename = "solvespace_browser-"; + //downloadfilename += `${GetCurrentDateTimeString()}.slvs`; + let downloadfilename = filename; linkElem.setAttribute("download", downloadfilename); linkElem.setAttribute("href", blobURL); // WORKAROUND: FIXME(emscripten) diff --git a/src/solvespace.cpp b/src/solvespace.cpp index fd0cb437..b0e48016 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -702,6 +702,9 @@ void SolveSpaceUI::MenuFile(Command id) { if(dialog->RunModal()) { dialog->FreezeChoices(settings, "ExportImage"); SS.ExportAsPngTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } } break; } @@ -727,6 +730,9 @@ void SolveSpaceUI::MenuFile(Command id) { } SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } break; } @@ -739,6 +745,9 @@ void SolveSpaceUI::MenuFile(Command id) { dialog->FreezeChoices(settings, "ExportWireframe"); SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } break; } @@ -751,6 +760,9 @@ void SolveSpaceUI::MenuFile(Command id) { dialog->FreezeChoices(settings, "ExportSection"); SS.ExportSectionTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } break; } @@ -763,6 +775,10 @@ void SolveSpaceUI::MenuFile(Command id) { dialog->FreezeChoices(settings, "ExportMesh"); SS.ExportMeshTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + break; } @@ -776,6 +792,9 @@ void SolveSpaceUI::MenuFile(Command id) { StepFileWriter sfw = {}; sfw.ExportSurfacesTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } break; }