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