Web: Improve file dialog.

pull/1310/head
verylowfreq 2022-08-13 21:10:44 +09:00 committed by ruevs
parent 4981570844
commit 1603402df2
7 changed files with 677 additions and 146 deletions

View File

@ -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)

View File

@ -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<void>("addEventListener", val("trigger"), Wrap(&responseFuncs.back()));
static std::function<void()> updateShowFlagFunc = [this] {
this->is_shown = false;
};
htmlButton.call<void>("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<void()>* callback = &responseFuncs.back();
htmlButton.call<void>("addEventListener", val("trigger"), Wrap(callback));
htmlButtons.call<void>("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<val>("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<void>("dispose");
~FileDialogImplHtml() override {
dbp("FileDialogImplHtml::~FileDialogImplHtml()");
this->jsFileManagerUI.call<void>("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<void>("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<void>("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<std::string> 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<void>("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<bool>();
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<std::string>();
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<std::string> 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<void>("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<void>("setBasePath", val(basePathInFilesystem));
this->jsFileManagerUI.call<void>("show");
while (true) {
bool isShown = this->jsFileManagerUI.call<bool>("isShown");
if (!isShown) {
break;
} else {
emscripten_sleep(50);
}
}
dbp("FileSaveDialogImplHtml::RunModal() : dialog closed.");
std::string selectedFilename = this->jsFileManagerUI.call<std::string>("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<FileOpenDialogImplHtml>(new FileOpenDialogImplHtml());
dbp("CreateOpenFileDialog()");
return std::shared_ptr<FileDialogImplHtml>(new FileDialogImplHtml(FileDialogImplHtml::Modes::OPEN));
}
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
// FIXME(emscripten): implement
// dbp("CreateSaveFileDialog()");
return std::shared_ptr<FileSaveDummyDialogImplHtml>(new FileSaveDummyDialogImplHtml());
dbp("CreateSaveFileDialog()");
return std::shared_ptr<FileDialogImplHtml>(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);
}

View File

@ -6,6 +6,7 @@
--><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.1/css/all.css" integrity="sha384-O8whS3fhG2OnA5Kas0Y9l3cfpmYjapjI0E4theH4iuMD+pLhbf6JI0jIMfYcK3yZ" crossorigin="anonymous"><!--
--><link rel="stylesheet" href="solvespaceui.css"><!--
--><script src="solvespaceui.js"></script><!--
--><script src="filemanagerui.js"></script><!--
--></head><!--
--><body><!--
--><div id="splash">

View File

@ -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;

View File

@ -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 {

View File

@ -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)

View File

@ -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;
}