Files
CodenameEngine/source/lime/ui/FileDialog.hx
2025-04-04 22:33:28 +02:00

412 lines
11 KiB
Haxe

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 Event<Void->Void>();
/**
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 Event<Resource->Void>();
/**
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 Event<String->Void>();
/**
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 Event<String->Void>();
/**
Triggers when `browse()` is successful and `type` is `FileDialogType.OPEN_MULTIPLE`. The
`Array<String>` contains all selected file paths.
**/
public var onSelectMultiple = new Event<Array<String>->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<HLBytes> = 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<String> = 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
}
}