diff --git a/source/lime/ui/FileDialog.hx b/source/lime/ui/FileDialog.hx new file mode 100644 index 00000000..418d7119 --- /dev/null +++ b/source/lime/ui/FileDialog.hx @@ -0,0 +1,411 @@ +package lime.ui; + +import haxe.io.Bytes; +import haxe.io.Path; +import lime._internal.backend.native.NativeCFFI; +import lime.app.Event; +import lime.graphics.Image; +import lime.system.BackgroundWorker; +import lime.utils.ArrayBuffer; +import lime.utils.Resource; +#if hl +import hl.Bytes as HLBytes; +import hl.NativeArray; +#end +#if sys +import sys.io.File; +#end +#if (js && html5) +import js.html.Blob; +#end + +/** + Simple file dialog used for asking user where to save a file, or select files to open. + + Example usage: + ```haxe + var fileDialog = new FileDialog(); + + fileDialog.onCancel.add( () -> trace("Canceled.") ); + + fileDialog.onSave.add( path -> trace("File saved in " + path) ); + + fileDialog.onOpen.add( res -> trace("Size of the file = " + (res:haxe.io.Bytes).length) ); + + if ( fileDialog.open("jpg", null, "Load file") ) + trace("File dialog opened, waiting for selection..."); + else + trace("This dialog is unsupported."); + ``` + + Availability note: most file dialog operations are only available on desktop targets, though + `save()` is also available in HTML5. +**/ +#if !lime_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(lime._internal.backend.native.NativeCFFI) +@:access(lime.graphics.Image) +class FileDialog +{ + /** + Triggers when the user clicks "Cancel" during any operation, or when a function is unsupported + (such as `open()` on HTML5). + **/ + public var onCancel = new EventVoid>(); + + /** + Triggers when `open()` is successful. The `lime.utils.Resource` contains the file's data, and can + be implicitly cast to `haxe.io.Bytes`. + **/ + public var onOpen = new EventVoid>(); + + /** + Triggers when `open()` is successful. The `lime.utils.Resource` contains the file's data, and can + be implicitly cast to `haxe.io.Bytes`, the String is the path to the file. + **/ + public var onOpenFile = new Event<(Resource, String)->Void>(); // Added by @NeeEoo + + /** + Triggers when `save()` is successful. The `String` is the path to the saved file. + **/ + public var onSave = new EventVoid>(); + + /** + Triggers when `browse()` is successful and `type` is anything other than + `FileDialogType.OPEN_MULTIPLE`. The `String` is the path to the selected file. + **/ + public var onSelect = new EventVoid>(); + + /** + Triggers when `browse()` is successful and `type` is `FileDialogType.OPEN_MULTIPLE`. The + `Array` contains all selected file paths. + **/ + public var onSelectMultiple = new Event->Void>(); + + public function new() {} + + /** + Opens a file selection dialog. If successful, either `onSelect` or `onSelectMultiple` will trigger + with the result(s). + + This function only works on desktop targets, and will return `false` otherwise. + @param type Type of the file dialog: `OPEN`, `SAVE`, `OPEN_DIRECTORY` or `OPEN_MULTIPLE`. + @param filter A filter to use when browsing. Asterisks are treated as wildcards. For example, + `"*.jpg"` will match any file ending in `.jpg`. + @param defaultPath The directory in which to start browsing and/or the default filename to + suggest. Defaults to `Sys.getCwd()`, with no default filename. + @param title The title to give the dialog window. + @return Whether `browse()` is supported on this target. + **/ + public function browse(type:FileDialogType = null, filter:String = null, defaultPath:String = null, title:String = null):Bool + { + if (type == null) type = FileDialogType.OPEN; + + #if desktop + var worker = new BackgroundWorker(); + + worker.doWork.add(function(_) + { + switch (type) + { + case OPEN: + #if linux + if (title == null) title = "Open File"; + #end + + var path = null; + #if (!macro && lime_cffi) + #if hl + var bytes = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); + if (bytes != null) + { + path = @:privateAccess String.fromUTF8(cast bytes); + } + #else + path = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); + #end + #end + + worker.sendComplete(path); + + case OPEN_MULTIPLE: + #if linux + if (title == null) title = "Open Files"; + #end + + var paths = null; + #if (!macro && lime_cffi) + #if hl + var bytes:NativeArray = cast NativeCFFI.lime_file_dialog_open_files(title, filter, defaultPath); + if (bytes != null) + { + paths = []; + for (i in 0...bytes.length) + { + paths[i] = @:privateAccess String.fromUTF8(bytes[i]); + } + } + #else + paths = NativeCFFI.lime_file_dialog_open_files(title, filter, defaultPath); + #end + #end + + worker.sendComplete(paths); + + case OPEN_DIRECTORY: + #if linux + if (title == null) title = "Open Directory"; + #end + + var path = null; + #if (!macro && lime_cffi) + #if hl + var bytes = NativeCFFI.lime_file_dialog_open_directory(title, filter, defaultPath); + if (bytes != null) + { + path = @:privateAccess String.fromUTF8(cast bytes); + } + #else + path = NativeCFFI.lime_file_dialog_open_directory(title, filter, defaultPath); + #end + #end + + worker.sendComplete(path); + + case SAVE: + #if linux + if (title == null) title = "Save File"; + #end + + var path = null; + #if (!macro && lime_cffi) + #if hl + var bytes = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); + if (bytes != null) + { + path = @:privateAccess String.fromUTF8(cast bytes); + } + #else + path = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); + #end + #end + + worker.sendComplete(path); + } + }); + + worker.onComplete.add(function(result) + { + switch (type) + { + case OPEN, OPEN_DIRECTORY, SAVE: + var path:String = cast result; + + if (path != null) + { + // Makes sure the filename ends with extension + if (type == SAVE && filter != null && path.indexOf(".") == -1) + { + path += "." + filter; + } + + onSelect.dispatch(path); + } + else + { + onCancel.dispatch(); + } + + case OPEN_MULTIPLE: + var paths:Array = cast result; + + if (paths != null && paths.length > 0) + { + onSelectMultiple.dispatch(paths); + } + else + { + onCancel.dispatch(); + } + } + }); + + worker.run(); + + return true; + #else + onCancel.dispatch(); + return false; + #end + } + + /** + Shows an open file dialog. If successful, `onOpen` will trigger with the file contents. + + This function only works on desktop targets, and will return `false` otherwise. + @param filter A filter to use when browsing. Asterisks are treated as wildcards. For example, + `"*.jpg"` will match any file ending in `.jpg`. + @param defaultPath The directory in which to start browsing and/or the default filename to + suggest. Defaults to `Sys.getCwd()`, with no default filename. + @param title The title to give the dialog window. + @return Whether `open()` is supported on this target. + **/ + public function open(filter:String = null, defaultPath:String = null, title:String = null):Bool + { + #if (desktop && sys) + var worker = new BackgroundWorker(); + + worker.doWork.add(function(_) + { + #if linux + if (title == null) title = "Open File"; + #end + + var path = null; + #if (!macro && lime_cffi) + #if hl + var bytes = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); + if (bytes != null) path = @:privateAccess String.fromUTF8(cast bytes); + #else + path = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); + #end + #end + + worker.sendComplete(path); + }); + + worker.onComplete.add(function(path:String) + { + if (path != null) + { + try + { + var data = File.getBytes(path); + onOpen.dispatch(data); + onOpenFile.dispatch(data, path); // Added by @NeeEoo + return; + } + catch (e:Dynamic) {} + } + + onCancel.dispatch(); + }); + + worker.run(); + + return true; + #else + onCancel.dispatch(); + return false; + #end + } + + /** + Shows an open file dialog. If successful, `onSave` will trigger with the selected path. + + This function only works on desktop and HMTL5 targets, and will return `false` otherwise. + @param data The file contents, in `haxe.io.Bytes` format. (Implicit casting possible.) + @param filter A filter to use when browsing. Asterisks are treated as wildcards. For example, + `"*.jpg"` will match any file ending in `.jpg`. Used only if targeting deskop. + @param defaultPath The directory in which to start browsing and/or the default filename to + suggest. When targeting destkop, this defaults to `Sys.getCwd()` with no default filename. When targeting + HTML5, this defaults to the browser's download directory, with a default filename based on the MIME type. + @param title The title to give the dialog window. + @param type The default MIME type of the file, in case the type can't be determined from the + file data. Used only if targeting HTML5. + @return Whether `save()` is supported on this target. + **/ + public function save(data:Resource, filter:String = null, defaultPath:String = null, title:String = null, type:String = "application/octet-stream"):Bool + { + if (data == null) + { + onCancel.dispatch(); + return false; + } + + #if (desktop && sys) + var worker = new BackgroundWorker(); + + worker.doWork.add(function(_) + { + #if linux + if (title == null) title = "Save File"; + #end + + var path = null; + #if (!macro && lime_cffi) + #if hl + var bytes = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); + path = @:privateAccess String.fromUTF8(cast bytes); + #else + path = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); + #end + #end + + worker.sendComplete(path); + }); + + worker.onComplete.add(function(path:String) + { + if (path != null) + { + try + { + File.saveBytes(path, data); + onSave.dispatch(path); + return; + } + catch (e:Dynamic) {} + } + + onCancel.dispatch(); + }); + + worker.run(); + + return true; + #elseif (js && html5) + // TODO: Cleaner API for mimeType detection + + var defaultExtension = ""; + + if (Image.__isPNG(data)) + { + type = "image/png"; + defaultExtension = ".png"; + } + else if (Image.__isJPG(data)) + { + type = "image/jpeg"; + defaultExtension = ".jpg"; + } + else if (Image.__isGIF(data)) + { + type = "image/gif"; + defaultExtension = ".gif"; + } + else if (Image.__isWebP(data)) + { + type = "image/webp"; + defaultExtension = ".webp"; + } + + var path = defaultPath != null ? Path.withoutDirectory(defaultPath) : "download" + defaultExtension; + var buffer = (data : Bytes).getData(); + buffer = buffer.slice(0, (data : Bytes).length); + + #if commonjs + untyped #if haxe4 js.Syntax.code #else __js__ #end ("require ('file-saver')")(new Blob([buffer], {type: type}), path, true); + #else + untyped window.saveAs(new Blob([buffer], {type: type}), path, true); + #end + onSave.dispatch(path); + return true; + #else + onCancel.dispatch(); + return false; + #end + } +}