Added image/video control and property manipulation
This commit is contained in:
@@ -3,6 +3,8 @@ package org.openautonomousconnection.luascript.events;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -17,12 +19,18 @@ public final class JavaToLua {
|
||||
public static LuaValue coerce(Object v) {
|
||||
if (v == null) return LuaValue.NIL;
|
||||
if (v instanceof LuaValue lv) return lv;
|
||||
|
||||
if (v instanceof String s) return LuaValue.valueOf(s);
|
||||
if (v instanceof Boolean b) return LuaValue.valueOf(b);
|
||||
if (v instanceof Integer i) return LuaValue.valueOf(i);
|
||||
if (v instanceof Long l) return LuaValue.valueOf(l);
|
||||
if (v instanceof Float f) return LuaValue.valueOf(f);
|
||||
if (v instanceof Double d) return LuaValue.valueOf(d);
|
||||
|
||||
if (v instanceof Byte n) return LuaValue.valueOf(n.intValue());
|
||||
if (v instanceof Short n) return LuaValue.valueOf(n.intValue());
|
||||
if (v instanceof Integer n) return LuaValue.valueOf(n);
|
||||
if (v instanceof Long n) return LuaValue.valueOf(n);
|
||||
if (v instanceof Float n) return LuaValue.valueOf(n.doubleValue());
|
||||
if (v instanceof Double n) return LuaValue.valueOf(n);
|
||||
|
||||
if (v instanceof Number n) return LuaValue.valueOf(n.doubleValue());
|
||||
|
||||
if (v instanceof Map<?, ?> m) {
|
||||
LuaTable t = new LuaTable();
|
||||
@@ -43,6 +51,24 @@ public final class JavaToLua {
|
||||
return t;
|
||||
}
|
||||
|
||||
if (v instanceof Collection<?> col) {
|
||||
LuaTable t = new LuaTable();
|
||||
int i = 1;
|
||||
for (Object o : col) {
|
||||
t.set(i++, coerce(o));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
if (v.getClass().isArray()) {
|
||||
LuaTable t = new LuaTable();
|
||||
int len = Array.getLength(v);
|
||||
for (int i = 0; i < len; i++) {
|
||||
t.set(i + 1, coerce(Array.get(v, i)));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
return LuaValue.valueOf(String.valueOf(v));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import org.openautonomousconnection.luascript.hosts.AudioHost;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* JavaFX MediaPlayer based AudioHost.
|
||||
*
|
||||
* <p>Note: JavaFX Media does not reliably use {@link java.net.URLStreamHandler} for custom schemes.
|
||||
* Therefore, for {@code web://} this host resolves via {@link URLConnection} (your installed handler),
|
||||
* spools to a temporary file and plays that file via {@code file://}.</p>
|
||||
*/
|
||||
public final class FxAudioHost implements AudioHost {
|
||||
|
||||
private static final Set<String> AUDIO_EXTENSIONS = Set.of(
|
||||
"mp3", "wav", "ogg", "m4a", "opus", "web"
|
||||
);
|
||||
|
||||
private volatile MediaPlayer player;
|
||||
private volatile boolean loop;
|
||||
private volatile double volume = 1.0;
|
||||
|
||||
private volatile Path lastTempFile;
|
||||
|
||||
@Override
|
||||
public void playFile(File file) {
|
||||
Objects.requireNonNull(file, "file");
|
||||
|
||||
if (!file.isFile()) {
|
||||
throw new IllegalArgumentException("Audio file not found: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
String ext = MediaExtensions.extensionOf(file.getName());
|
||||
ensureAllowedAudioExtension(ext, file.getName());
|
||||
|
||||
playUri(file.toURI());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playUrl(String url) {
|
||||
Objects.requireNonNull(url, "url");
|
||||
String u = url.trim();
|
||||
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
|
||||
|
||||
URI uri = URI.create(u);
|
||||
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
|
||||
String ext = MediaExtensions.extensionOf(uri.getPath());
|
||||
ensureAllowedAudioExtension(ext, uri.getPath());
|
||||
|
||||
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
|
||||
playUri(uri);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("web".equals(scheme)) {
|
||||
Path tmp = null;
|
||||
try {
|
||||
tmp = downloadToTempFile(u, ext);
|
||||
rememberTemp(tmp);
|
||||
playUri(tmp.toUri());
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw new RuntimeException("Failed to load web audio: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
Platform.runLater(() -> {
|
||||
MediaPlayer p = player;
|
||||
if (p != null) p.pause();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
Platform.runLater(this::stopInternal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(double volume) {
|
||||
double v = clamp01(volume);
|
||||
this.volume = v;
|
||||
Platform.runLater(() -> {
|
||||
MediaPlayer p = player;
|
||||
if (p != null) p.setVolume(v);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoop(boolean loop) {
|
||||
this.loop = loop;
|
||||
Platform.runLater(() -> {
|
||||
MediaPlayer p = player;
|
||||
if (p != null) p.setCycleCount(loop ? MediaPlayer.INDEFINITE : 1);
|
||||
});
|
||||
}
|
||||
|
||||
private void playUri(URI uri) {
|
||||
Platform.runLater(() -> {
|
||||
stopInternal();
|
||||
|
||||
Media media = new Media(uri.toString());
|
||||
MediaPlayer p = new MediaPlayer(media);
|
||||
|
||||
p.setCycleCount(loop ? MediaPlayer.INDEFINITE : 1);
|
||||
p.setVolume(volume);
|
||||
|
||||
p.setOnEndOfMedia(() -> {
|
||||
if (!loop) {
|
||||
try {
|
||||
p.dispose();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.player = p;
|
||||
p.play();
|
||||
});
|
||||
}
|
||||
|
||||
private void stopInternal() {
|
||||
MediaPlayer p = this.player;
|
||||
this.player = null;
|
||||
|
||||
if (p != null) {
|
||||
try {
|
||||
p.stop();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
p.dispose();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
Path tmp = this.lastTempFile;
|
||||
this.lastTempFile = null;
|
||||
safeDelete(tmp);
|
||||
}
|
||||
|
||||
private static Path downloadToTempFile(String url, String ext) throws IOException {
|
||||
URL u = new URL(url);
|
||||
URLConnection con = u.openConnection();
|
||||
con.setUseCaches(false);
|
||||
|
||||
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
|
||||
Path tmp = Files.createTempFile("oac-audio-", "." + safeExt);
|
||||
|
||||
try (InputStream in = con.getInputStream()) {
|
||||
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw e;
|
||||
}
|
||||
|
||||
Files.write(tmp, new byte[0], StandardOpenOption.APPEND);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private void rememberTemp(Path tmp) {
|
||||
Path prev = this.lastTempFile;
|
||||
this.lastTempFile = tmp;
|
||||
safeDelete(prev);
|
||||
}
|
||||
|
||||
private static void safeDelete(Path p) {
|
||||
if (p == null) return;
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
private static double clamp01(double v) {
|
||||
if (v < 0.0) return 0.0;
|
||||
if (v > 1.0) return 1.0;
|
||||
return v;
|
||||
}
|
||||
|
||||
private static void ensureAllowedAudioExtension(String ext, String source) {
|
||||
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
|
||||
if (e.isEmpty() || !AUDIO_EXTENSIONS.contains(e)) {
|
||||
throw new IllegalArgumentException("Unsupported audio format '" + e + "' for: " + source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
/**
|
||||
* DomHost implementation backed by JavaFX WebView's W3C DOM (WebEngine#getDocument()).
|
||||
*
|
||||
* <p>No jsoup and no JavaScript. All operations are performed via W3C DOM APIs.</p>
|
||||
* <p>Uses W3C DOM for structure/attributes and a small JavaScript bridge for DOM properties and method calls
|
||||
* (required for HTMLMediaElement like video/audio). Lua remains the scripting language; JavaScript is only
|
||||
* used internally to access DOM properties/methods that are not available via W3C DOM APIs.</p>
|
||||
*
|
||||
* <p>Element identity is the {@code id} attribute. This host auto-assigns stable ids to elements that
|
||||
* do not have one, ensuring addressability for Lua bindings and event routing.</p>
|
||||
@@ -77,6 +79,24 @@ public final class FxDomHost implements DomHost {
|
||||
return v;
|
||||
}
|
||||
|
||||
private static String requireId(String id) {
|
||||
if (id == null) throw new IllegalArgumentException("elementId is null");
|
||||
String v = id.trim();
|
||||
if (v.isEmpty()) throw new IllegalArgumentException("elementId is blank");
|
||||
return v;
|
||||
}
|
||||
|
||||
private static String requireJsIdentifier(String s, String label) {
|
||||
if (s == null) throw new IllegalArgumentException(label + " is null");
|
||||
String v = s.trim();
|
||||
if (v.isEmpty()) throw new IllegalArgumentException(label + " is blank");
|
||||
// Prevent JS injection: only simple identifier.
|
||||
if (!v.matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
|
||||
throw new IllegalArgumentException(label + " must be a JS identifier: " + v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures every element has a stable id.
|
||||
*/
|
||||
@@ -314,6 +334,77 @@ public final class FxDomHost implements DomHost {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(String elementId, String property, Object value) {
|
||||
final String id = requireId(elementId);
|
||||
final String prop = requireJsIdentifier(property, "property");
|
||||
final String jsValue = toJsLiteral(value);
|
||||
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
requireDocument();
|
||||
ensureJsEnabled();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var el = document.getElementById(" + toJsLiteral(id) + ");"
|
||||
+ " if(!el) throw new Error('Unknown element id: ' + " + toJsLiteral(id) + ");"
|
||||
+ " el[" + toJsLiteral(prop) + "] = " + jsValue + ";"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String elementId, String property) {
|
||||
final String id = requireId(elementId);
|
||||
final String prop = requireJsIdentifier(property, "property");
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> {
|
||||
requireDocument();
|
||||
ensureJsEnabled();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var el = document.getElementById(" + toJsLiteral(id) + ");"
|
||||
+ " if(!el) throw new Error('Unknown element id: ' + " + toJsLiteral(id) + ");"
|
||||
+ " return el[" + toJsLiteral(prop) + "];"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object call(String elementId, String method, Object... args) {
|
||||
final String id = requireId(elementId);
|
||||
final String m = requireJsIdentifier(method, "method");
|
||||
final Object[] safeArgs = (args == null) ? new Object[0] : args.clone();
|
||||
final String argvLiteral = toJsArrayLiteral(safeArgs);
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> {
|
||||
requireDocument();
|
||||
ensureJsEnabled();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var el = document.getElementById(" + toJsLiteral(id) + ");"
|
||||
+ " if(!el) throw new Error('Unknown element id: ' + " + toJsLiteral(id) + ");"
|
||||
+ " var fn = el[" + toJsLiteral(m) + "];"
|
||||
+ " if(typeof fn !== 'function') throw new Error('Not a function: ' + " + toJsLiteral(m) + ");"
|
||||
+ " return fn.apply(el, " + argvLiteral + ");"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the current document (FX thread access required by callers).
|
||||
*
|
||||
@@ -339,6 +430,13 @@ public final class FxDomHost implements DomHost {
|
||||
return el;
|
||||
}
|
||||
|
||||
private void ensureJsEnabled() {
|
||||
// Required for HTMLMediaElement and DOM property access beyond W3C DOM.
|
||||
if (!engine.isJavaScriptEnabled()) {
|
||||
engine.setJavaScriptEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateUniqueId(Document doc) {
|
||||
while (true) {
|
||||
String id = "__auto_" + autoIdSeq.getAndIncrement();
|
||||
@@ -353,4 +451,104 @@ public final class FxDomHost implements DomHost {
|
||||
}
|
||||
return generateUniqueId(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Java value into a safe JavaScript literal.
|
||||
*
|
||||
* <p>Supported:</p>
|
||||
* <ul>
|
||||
* <li>null</li>
|
||||
* <li>String</li>
|
||||
* <li>Boolean</li>
|
||||
* <li>Number (finite)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param v value
|
||||
* @return JS literal
|
||||
*/
|
||||
private static String toJsLiteral(Object v) {
|
||||
if (v == null) return "null";
|
||||
|
||||
if (v instanceof String s) {
|
||||
return "'" + escapeJsSingleQuotedString(s) + "'";
|
||||
}
|
||||
|
||||
if (v instanceof Boolean b) {
|
||||
return b ? "true" : "false";
|
||||
}
|
||||
|
||||
if (v instanceof Byte || v instanceof Short || v instanceof Integer || v instanceof Long) {
|
||||
return String.valueOf(((Number) v).longValue());
|
||||
}
|
||||
|
||||
if (v instanceof Float || v instanceof Double) {
|
||||
double d = ((Number) v).doubleValue();
|
||||
if (!Double.isFinite(d)) {
|
||||
throw new IllegalArgumentException("Non-finite number is not supported for JS literal: " + d);
|
||||
}
|
||||
// Use plain Java formatting; JS accepts it.
|
||||
return Double.toString(d);
|
||||
}
|
||||
|
||||
if (v instanceof Number n) {
|
||||
double d = n.doubleValue();
|
||||
if (!Double.isFinite(d)) {
|
||||
throw new IllegalArgumentException("Non-finite number is not supported for JS literal: " + d);
|
||||
}
|
||||
return Double.toString(d);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported value type for JS literal: " + v.getClass().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a JavaScript array literal from Java arguments.
|
||||
*
|
||||
* @param args args (nullable)
|
||||
* @return JS array literal
|
||||
*/
|
||||
private static String toJsArrayLiteral(Object[] args) {
|
||||
if (args == null || args.length == 0) return "[]";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) sb.append(',');
|
||||
sb.append(toJsLiteral(args[i]));
|
||||
}
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for inclusion inside a single-quoted JavaScript string literal.
|
||||
*
|
||||
* @param s raw string
|
||||
* @return escaped string
|
||||
*/
|
||||
private static String escapeJsSingleQuotedString(String s) {
|
||||
if (s == null || s.isEmpty()) return "";
|
||||
StringBuilder out = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '\'' -> out.append("\\'");
|
||||
case '\\' -> out.append("\\\\");
|
||||
case '\n' -> out.append("\\n");
|
||||
case '\r' -> out.append("\\r");
|
||||
case '\t' -> out.append("\\t");
|
||||
case '\u0000' -> out.append("\\0");
|
||||
case '\u2028' -> out.append("\\u2028");
|
||||
case '\u2029' -> out.append("\\u2029");
|
||||
default -> {
|
||||
// Keep printable chars; escape other control chars.
|
||||
if (c < 0x20) {
|
||||
out.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.ImageHost;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* ImageHost implementation that renders images inside the WebView DOM using an {@code <img>} element.
|
||||
*
|
||||
* <p>No JavaFX overlay panes are used. Positioning is done by setting CSS via {@code style} attribute.</p>
|
||||
*
|
||||
* <p>For {@code web://} URLs this host downloads the resource via {@link URLConnection} (your installed handler),
|
||||
* stores it as a temporary file, then sets {@code src=file://...} because WebView won't reliably load custom schemes.</p>
|
||||
*/
|
||||
public final class FxImageHost implements ImageHost {
|
||||
|
||||
private static final Set<String> IMAGE_EXTENSIONS = Set.of(
|
||||
"png", "jpg", "jpeg", "ico", "bmp", "avif", "heif", "heic", "webp"
|
||||
);
|
||||
|
||||
private static final String IMG_ID = "__oac_image";
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
private volatile double x;
|
||||
private volatile double y;
|
||||
private volatile double w = 320;
|
||||
private volatile double h = 240;
|
||||
|
||||
private volatile boolean preserveRatio = true;
|
||||
private volatile boolean smooth = true;
|
||||
private volatile double opacity = 1.0;
|
||||
|
||||
private volatile Path lastTempFile;
|
||||
|
||||
/**
|
||||
* Creates a new image host.
|
||||
*
|
||||
* @param engine web engine
|
||||
* @param dom fx dom host
|
||||
*/
|
||||
public FxImageHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
// Ensure element exists once document is present; if not yet loaded, calls will create on demand.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFile(File file) {
|
||||
Objects.requireNonNull(file, "file");
|
||||
if (!file.isFile()) {
|
||||
throw new IllegalArgumentException("Image file not found: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
String ext = MediaExtensions.extensionOf(file.getName());
|
||||
ensureAllowed(ext, "image", file.getName());
|
||||
|
||||
URI uri = file.toURI();
|
||||
setSource(uri.toString(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showUrl(String url) {
|
||||
Objects.requireNonNull(url, "url");
|
||||
String u = url.trim();
|
||||
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
|
||||
|
||||
URI uri = URI.create(u);
|
||||
String ext = MediaExtensions.extensionOf(uri.getPath());
|
||||
ensureAllowed(ext, "image", uri.getPath());
|
||||
|
||||
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
|
||||
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
|
||||
setSource(uri.toString(), null);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("web".equals(scheme)) {
|
||||
Path tmp = null;
|
||||
try {
|
||||
tmp = downloadToTempFile(u, ext, "oac-img-");
|
||||
rememberTemp(tmp);
|
||||
setSource(tmp.toUri().toString(), tmp);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw new RuntimeException("Failed to load web image: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element img = ensureImgElement();
|
||||
img.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
|
||||
img.removeAttribute("src");
|
||||
});
|
||||
|
||||
Path tmp = this.lastTempFile;
|
||||
this.lastTempFile = null;
|
||||
safeDelete(tmp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRect(double x, double y, double w, double h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = Math.max(0.0, w);
|
||||
this.h = Math.max(0.0, h);
|
||||
applyStyle(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPreserveRatio(boolean preserve) {
|
||||
this.preserveRatio = preserve;
|
||||
applyStyle(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSmooth(boolean smooth) {
|
||||
this.smooth = smooth;
|
||||
applyStyle(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOpacity(double opacity) {
|
||||
this.opacity = clamp01(opacity);
|
||||
applyStyle(true);
|
||||
}
|
||||
|
||||
private void setSource(String src, Path tempFileKept) {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element img = ensureImgElement();
|
||||
img.setAttribute("src", src);
|
||||
img.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
|
||||
});
|
||||
|
||||
// Keep temp file reference (so previous gets cleaned up)
|
||||
if (tempFileKept != null) {
|
||||
rememberTemp(tempFileKept);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyStyle(boolean visible) {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element img = ensureImgElement();
|
||||
String src = img.getAttribute("src");
|
||||
boolean show = visible && src != null && !src.isBlank();
|
||||
img.setAttribute("style", mergeStyle(baseStyle(show), rectStyle(), visualStyle()));
|
||||
});
|
||||
}
|
||||
|
||||
private Element ensureImgElement() {
|
||||
Document doc = dom.requireDocument();
|
||||
Element img = doc.getElementById(IMG_ID);
|
||||
if (img != null) return img;
|
||||
|
||||
Element body = (Element) doc.getElementsByTagName("body").item(0);
|
||||
if (body == null) throw new IllegalStateException("No <body> element available");
|
||||
|
||||
img = doc.createElement("img");
|
||||
img.setAttribute("id", IMG_ID);
|
||||
img.setAttribute("draggable", "false");
|
||||
img.setAttribute("alt", "");
|
||||
|
||||
body.appendChild(img);
|
||||
return img;
|
||||
}
|
||||
|
||||
private String baseStyle(boolean visible) {
|
||||
return "position:fixed;"
|
||||
+ "left:0;top:0;"
|
||||
+ "z-index:2147483647;"
|
||||
+ "display:" + (visible ? "block" : "none") + ";";
|
||||
}
|
||||
|
||||
private String rectStyle() {
|
||||
return "left:" + x + "px;"
|
||||
+ "top:" + y + "px;"
|
||||
+ "width:" + w + "px;"
|
||||
+ "height:" + h + "px;";
|
||||
}
|
||||
|
||||
private String visualStyle() {
|
||||
String fit;
|
||||
if (preserveRatio) {
|
||||
fit = "object-fit:contain;";
|
||||
} else {
|
||||
fit = "object-fit:fill;";
|
||||
}
|
||||
|
||||
String smoothing = smooth ? "image-rendering:auto;" : "image-rendering:pixelated;";
|
||||
return fit + smoothing + "opacity:" + opacity + ";";
|
||||
}
|
||||
|
||||
private static String mergeStyle(String... parts) {
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
for (String p : parts) {
|
||||
if (p == null || p.isBlank()) continue;
|
||||
String s = p.trim();
|
||||
if (!s.endsWith(";")) s += ";";
|
||||
sb.append(s);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static Path downloadToTempFile(String url, String ext, String prefix) throws IOException {
|
||||
URL u = new URL(url);
|
||||
URLConnection con = u.openConnection();
|
||||
con.setUseCaches(false);
|
||||
|
||||
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
|
||||
Path tmp = Files.createTempFile(prefix, "." + safeExt);
|
||||
|
||||
try (InputStream in = con.getInputStream()) {
|
||||
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private void rememberTemp(Path tmp) {
|
||||
Path prev = this.lastTempFile;
|
||||
this.lastTempFile = tmp;
|
||||
safeDelete(prev);
|
||||
}
|
||||
|
||||
private static void safeDelete(Path p) {
|
||||
if (p == null) return;
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureAllowed(String ext, String kind, String source) {
|
||||
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
|
||||
if (e.isEmpty() || !IMAGE_EXTENSIONS.contains(e)) {
|
||||
throw new IllegalArgumentException("Unsupported " + kind + " format '" + e + "' for: " + source);
|
||||
}
|
||||
}
|
||||
|
||||
private static double clamp01(double v) {
|
||||
if (v < 0.0) return 0.0;
|
||||
if (v > 1.0) return 1.0;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.VideoHost;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* VideoHost implementation that renders video inside the WebView DOM using a {@code <video>} element.
|
||||
*
|
||||
* <p>No JavaFX overlay panes are used. Positioning is done by setting CSS via {@code style} attribute.</p>
|
||||
*
|
||||
* <p>Playback control is done via {@link FxDomHost#setProperty(String, String, Object)} and
|
||||
* {@link FxDomHost#call(String, String, Object...)}, i.e. HTMLMediaElement methods.</p>
|
||||
*
|
||||
* <p>For {@code web://} URLs this host downloads the resource via {@link URLConnection} (your installed handler),
|
||||
* stores it as a temporary file, then sets {@code src=file://...} because WebView won't reliably load custom schemes.</p>
|
||||
*/
|
||||
public final class FxVideoHost implements VideoHost {
|
||||
|
||||
private static final Set<String> VIDEO_EXTENSIONS = Set.of(
|
||||
"mp4", "mov", "ogg", "ogv", "gif", "gifv", "avi", "m4v"
|
||||
);
|
||||
|
||||
private static final String VIDEO_ID = "__oac_video";
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
private volatile boolean loop;
|
||||
private volatile double volume = 1.0;
|
||||
|
||||
private volatile double x;
|
||||
private volatile double y;
|
||||
private volatile double w = 640;
|
||||
private volatile double h = 360;
|
||||
|
||||
private volatile Path lastTempFile;
|
||||
|
||||
/**
|
||||
* Creates a new video host.
|
||||
*
|
||||
* @param engine web engine
|
||||
* @param dom fx dom host
|
||||
*/
|
||||
public FxVideoHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playFile(File file) {
|
||||
Objects.requireNonNull(file, "file");
|
||||
if (!file.isFile()) {
|
||||
throw new IllegalArgumentException("Video file not found: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
String ext = MediaExtensions.extensionOf(file.getName());
|
||||
ensureAllowed(ext, "video", file.getName());
|
||||
|
||||
setSource(file.toURI().toString(), null);
|
||||
resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playUrl(String url) {
|
||||
Objects.requireNonNull(url, "url");
|
||||
String u = url.trim();
|
||||
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
|
||||
|
||||
URI uri = URI.create(u);
|
||||
String ext = MediaExtensions.extensionOf(uri.getPath());
|
||||
ensureAllowed(ext, "video", uri.getPath());
|
||||
|
||||
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
|
||||
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
|
||||
setSource(uri.toString(), null);
|
||||
resume();
|
||||
return;
|
||||
}
|
||||
|
||||
if ("web".equals(scheme)) {
|
||||
Path tmp = null;
|
||||
try {
|
||||
tmp = downloadToTempFile(u, ext, "oac-video-");
|
||||
rememberTemp(tmp);
|
||||
setSource(tmp.toUri().toString(), tmp);
|
||||
resume();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw new RuntimeException("Failed to load web video: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
ensureVideoElement();
|
||||
dom.call(VIDEO_ID, "pause");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
applyMediaProps();
|
||||
v.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
|
||||
dom.call(VIDEO_ID, "play");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
dom.call(VIDEO_ID, "pause");
|
||||
dom.setProperty(VIDEO_ID, "currentTime", 0);
|
||||
v.removeAttribute("src");
|
||||
v.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
|
||||
});
|
||||
|
||||
Path tmp = this.lastTempFile;
|
||||
this.lastTempFile = null;
|
||||
safeDelete(tmp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
v.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRect(double x, double y, double w, double h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = Math.max(0.0, w);
|
||||
this.h = Math.max(0.0, h);
|
||||
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
boolean visible = "block".equalsIgnoreCase(extractDisplay(v.getAttribute("style")));
|
||||
v.setAttribute("style", mergeStyle(baseStyle(visible), rectStyle(), visualStyle()));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(double volume) {
|
||||
this.volume = clamp01(volume);
|
||||
FxThreadBridge.runAndWait(this::applyMediaProps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoop(boolean loop) {
|
||||
this.loop = loop;
|
||||
FxThreadBridge.runAndWait(this::applyMediaProps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(double seconds) {
|
||||
double s = Math.max(0.0, seconds);
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
ensureVideoElement();
|
||||
dom.setProperty(VIDEO_ID, "currentTime", s);
|
||||
});
|
||||
}
|
||||
|
||||
private void setSource(String src, Path tempFileKept) {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
v.setAttribute("src", src);
|
||||
applyMediaProps();
|
||||
v.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
|
||||
});
|
||||
|
||||
if (tempFileKept != null) {
|
||||
rememberTemp(tempFileKept);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMediaProps() {
|
||||
ensureVideoElement();
|
||||
dom.setProperty(VIDEO_ID, "loop", loop);
|
||||
dom.setProperty(VIDEO_ID, "volume", volume);
|
||||
}
|
||||
|
||||
private Element ensureVideoElement() {
|
||||
Document doc = dom.requireDocument();
|
||||
Element v = doc.getElementById(VIDEO_ID);
|
||||
if (v != null) return v;
|
||||
|
||||
Element body = (Element) doc.getElementsByTagName("body").item(0);
|
||||
if (body == null) throw new IllegalStateException("No <body> element available");
|
||||
|
||||
v = doc.createElement("video");
|
||||
v.setAttribute("id", VIDEO_ID);
|
||||
// Default: no controls; script can enable via dom.setAttr(id,"controls","controls") if needed.
|
||||
v.setAttribute("preload", "metadata");
|
||||
|
||||
body.appendChild(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
private String baseStyle(boolean visible) {
|
||||
return "position:fixed;"
|
||||
+ "left:0;top:0;"
|
||||
+ "z-index:2147483647;"
|
||||
+ "background-color:black;"
|
||||
+ "display:" + (visible ? "block" : "none") + ";";
|
||||
}
|
||||
|
||||
private String rectStyle() {
|
||||
return "left:" + x + "px;"
|
||||
+ "top:" + y + "px;"
|
||||
+ "width:" + w + "px;"
|
||||
+ "height:" + h + "px;";
|
||||
}
|
||||
|
||||
private String visualStyle() {
|
||||
return "object-fit:contain;";
|
||||
}
|
||||
|
||||
private static String mergeStyle(String... parts) {
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
for (String p : parts) {
|
||||
if (p == null || p.isBlank()) continue;
|
||||
String s = p.trim();
|
||||
if (!s.endsWith(";")) s += ";";
|
||||
sb.append(s);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String extractDisplay(String style) {
|
||||
if (style == null || style.isBlank()) return "";
|
||||
String[] parts = style.split(";");
|
||||
for (String part : parts) {
|
||||
String p = part.trim();
|
||||
if (p.isEmpty()) continue;
|
||||
int idx = p.indexOf(':');
|
||||
if (idx <= 0) continue;
|
||||
String k = p.substring(0, idx).trim().toLowerCase(Locale.ROOT);
|
||||
if ("display".equals(k)) return p.substring(idx + 1).trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static Path downloadToTempFile(String url, String ext, String prefix) throws IOException {
|
||||
URL u = new URL(url);
|
||||
URLConnection con = u.openConnection();
|
||||
con.setUseCaches(false);
|
||||
|
||||
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
|
||||
Path tmp = Files.createTempFile(prefix, "." + safeExt);
|
||||
|
||||
try (InputStream in = con.getInputStream()) {
|
||||
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private void rememberTemp(Path tmp) {
|
||||
Path prev = this.lastTempFile;
|
||||
this.lastTempFile = tmp;
|
||||
safeDelete(prev);
|
||||
}
|
||||
|
||||
private static void safeDelete(Path p) {
|
||||
if (p == null) return;
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureAllowed(String ext, String kind, String source) {
|
||||
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
|
||||
if (e.isEmpty() || !VIDEO_EXTENSIONS.contains(e)) {
|
||||
throw new IllegalArgumentException("Unsupported " + kind + " format '" + e + "' for: " + source);
|
||||
}
|
||||
}
|
||||
|
||||
private static double clamp01(double v) {
|
||||
if (v < 0.0) return 0.0;
|
||||
if (v > 1.0) return 1.0;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Shared extension helper for media sources.
|
||||
*/
|
||||
public final class MediaExtensions {
|
||||
|
||||
/**
|
||||
* Extracts a lower-case extension without dot, or empty string if none.
|
||||
*
|
||||
* @param pathOrName path or file name
|
||||
* @return extension without dot, lower-case, or empty string
|
||||
*/
|
||||
public static String extensionOf(String pathOrName) {
|
||||
if (pathOrName == null) return "";
|
||||
String s = pathOrName.trim();
|
||||
if (s.isEmpty()) return "";
|
||||
|
||||
int q = s.indexOf('?');
|
||||
if (q >= 0) s = s.substring(0, q);
|
||||
int h = s.indexOf('#');
|
||||
if (h >= 0) s = s.substring(0, h);
|
||||
|
||||
int slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
|
||||
String name = (slash >= 0) ? s.substring(slash + 1) : s;
|
||||
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot < 0 || dot == name.length() - 1) return "";
|
||||
|
||||
return name.substring(dot + 1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,53 @@ package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Host services for audio playback.
|
||||
*/
|
||||
public interface AudioHost {
|
||||
|
||||
void play(File audioFile);
|
||||
/**
|
||||
* Plays an audio file from the local filesystem.
|
||||
*
|
||||
* @param file local audio file
|
||||
*/
|
||||
void playFile(File file);
|
||||
|
||||
/**
|
||||
* Plays an audio resource identified by a URL string.
|
||||
*
|
||||
* <p>Supported schemes depend on the host implementation. Typical schemes:</p>
|
||||
* <ul>
|
||||
* <li>http://, https://</li>
|
||||
* <li>file://</li>
|
||||
* <li>web:// (custom, via installed URL handler)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param url URL string
|
||||
*/
|
||||
void playUrl(String url);
|
||||
|
||||
/**
|
||||
* Pauses playback (if any).
|
||||
*/
|
||||
void pause();
|
||||
|
||||
/**
|
||||
* Stops playback (if any).
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Sets volume in range [0.0, 1.0].
|
||||
*
|
||||
* @param volume volume
|
||||
*/
|
||||
void setVolume(double volume);
|
||||
|
||||
/**
|
||||
* Enables/disables looping.
|
||||
*
|
||||
* @param loop true to loop
|
||||
*/
|
||||
void setLoop(boolean loop);
|
||||
}
|
||||
@@ -147,4 +147,19 @@ public interface DomHost {
|
||||
* @return list of ids (never null)
|
||||
*/
|
||||
List<String> queryByClass(String className);
|
||||
|
||||
/**
|
||||
* Sets a DOM property (e.g. video.currentTime, video.volume).
|
||||
*/
|
||||
void setProperty(String elementId, String property, Object value);
|
||||
|
||||
/**
|
||||
* Gets a DOM property.
|
||||
*/
|
||||
Object getProperty(String elementId, String property);
|
||||
|
||||
/**
|
||||
* Invokes a DOM method (e.g. video.play()).
|
||||
*/
|
||||
Object call(String elementId, String method, Object... args);
|
||||
}
|
||||
@@ -45,8 +45,27 @@ public interface HostServices {
|
||||
*/
|
||||
Optional<ConsoleHost> console();
|
||||
|
||||
/**
|
||||
* Returns an optional audio host.
|
||||
*
|
||||
* @return audio host
|
||||
*/
|
||||
Optional<AudioHost> audio();
|
||||
|
||||
/**
|
||||
* Returns an optional image host.
|
||||
*
|
||||
* @return image host
|
||||
*/
|
||||
Optional<ImageHost> image();
|
||||
|
||||
/**
|
||||
* Returns an optional video host.
|
||||
*
|
||||
* @return video host
|
||||
*/
|
||||
Optional<VideoHost> video();
|
||||
|
||||
/**
|
||||
* Simple immutable implementation.
|
||||
*/
|
||||
@@ -56,7 +75,10 @@ public interface HostServices {
|
||||
private final EventHost events;
|
||||
private final ResourceHost resources;
|
||||
private final ConsoleHost console;
|
||||
|
||||
private final AudioHost audioHost;
|
||||
private final ImageHost imageHost;
|
||||
private final VideoHost videoHost;
|
||||
|
||||
/**
|
||||
* Creates a HostServices container.
|
||||
@@ -66,14 +88,28 @@ public interface HostServices {
|
||||
* @param events event host
|
||||
* @param resources resource host
|
||||
* @param console console host
|
||||
* @param audioHost audio host
|
||||
* @param imageHost image host
|
||||
* @param videoHost video host
|
||||
*/
|
||||
public Default(UiHost ui, DomHost dom, EventHost events, ResourceHost resources, ConsoleHost console, AudioHost audioHost) {
|
||||
public Default(
|
||||
UiHost ui,
|
||||
DomHost dom,
|
||||
EventHost events,
|
||||
ResourceHost resources,
|
||||
ConsoleHost console,
|
||||
AudioHost audioHost,
|
||||
ImageHost imageHost,
|
||||
VideoHost videoHost
|
||||
) {
|
||||
this.ui = ui;
|
||||
this.dom = dom;
|
||||
this.events = events;
|
||||
this.resources = resources;
|
||||
this.console = console;
|
||||
this.audioHost = audioHost;
|
||||
this.imageHost = imageHost;
|
||||
this.videoHost = videoHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,6 +141,16 @@ public interface HostServices {
|
||||
public Optional<AudioHost> audio() {
|
||||
return Optional.ofNullable(audioHost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ImageHost> image() {
|
||||
return Optional.ofNullable(imageHost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<VideoHost> video() {
|
||||
return Optional.ofNullable(videoHost);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Host services for image rendering in an overlay layer above the WebView.
|
||||
*/
|
||||
public interface ImageHost {
|
||||
|
||||
/**
|
||||
* Shows an image from the local filesystem.
|
||||
*
|
||||
* @param file image file
|
||||
*/
|
||||
void showFile(File file);
|
||||
|
||||
/**
|
||||
* Shows an image from a URL string (http/https/file/web).
|
||||
*
|
||||
* @param url url string
|
||||
*/
|
||||
void showUrl(String url);
|
||||
|
||||
/**
|
||||
* Hides the current image overlay.
|
||||
*/
|
||||
void hide();
|
||||
|
||||
/**
|
||||
* Sets the drawing rectangle (in WebView pixel coordinates).
|
||||
*
|
||||
* @param x x
|
||||
* @param y y
|
||||
* @param w width
|
||||
* @param h height
|
||||
*/
|
||||
void setRect(double x, double y, double w, double h);
|
||||
|
||||
/**
|
||||
* If true, preserves aspect ratio within the rectangle.
|
||||
*
|
||||
* @param preserve preserve aspect ratio
|
||||
*/
|
||||
void setPreserveRatio(boolean preserve);
|
||||
|
||||
/**
|
||||
* If true, image is smoothed when scaled.
|
||||
*
|
||||
* @param smooth smooth
|
||||
*/
|
||||
void setSmooth(boolean smooth);
|
||||
|
||||
/**
|
||||
* Sets opacity in range [0..1].
|
||||
*
|
||||
* @param opacity opacity
|
||||
*/
|
||||
void setOpacity(double opacity);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Host services for video playback in an overlay layer above the WebView.
|
||||
*/
|
||||
public interface VideoHost {
|
||||
|
||||
/**
|
||||
* Plays a video from the local filesystem.
|
||||
*
|
||||
* @param file video file
|
||||
*/
|
||||
void playFile(File file);
|
||||
|
||||
/**
|
||||
* Plays a video from a URL string (http/https/file/web).
|
||||
*
|
||||
* @param url url string
|
||||
*/
|
||||
void playUrl(String url);
|
||||
|
||||
/**
|
||||
* Pauses playback.
|
||||
*/
|
||||
void pause();
|
||||
|
||||
/**
|
||||
* Resumes playback (if paused).
|
||||
*/
|
||||
void resume();
|
||||
|
||||
/**
|
||||
* Stops playback and hides the view.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Hides the video view (does not necessarily stop decoding).
|
||||
*/
|
||||
void hide();
|
||||
|
||||
/**
|
||||
* Sets the drawing rectangle (in WebView pixel coordinates).
|
||||
*
|
||||
* @param x x
|
||||
* @param y y
|
||||
* @param w width
|
||||
* @param h height
|
||||
*/
|
||||
void setRect(double x, double y, double w, double h);
|
||||
|
||||
/**
|
||||
* Sets volume in range [0..1].
|
||||
*
|
||||
* @param volume volume
|
||||
*/
|
||||
void setVolume(double volume);
|
||||
|
||||
/**
|
||||
* Enables/disables looping.
|
||||
*
|
||||
* @param loop loop
|
||||
*/
|
||||
void setLoop(boolean loop);
|
||||
|
||||
/**
|
||||
* Seeks to a position in seconds.
|
||||
*
|
||||
* @param seconds seconds
|
||||
*/
|
||||
void seek(double seconds);
|
||||
}
|
||||
@@ -3,10 +3,7 @@ package org.openautonomousconnection.luascript.runtime;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
||||
import org.openautonomousconnection.luascript.fx.FxEventHost;
|
||||
import org.openautonomousconnection.luascript.fx.FxUiHost;
|
||||
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
|
||||
import org.openautonomousconnection.luascript.fx.*;
|
||||
import org.openautonomousconnection.luascript.hosts.AudioHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||
@@ -52,7 +49,7 @@ public final class FxLuaScriptEngine implements AutoCloseable {
|
||||
/**
|
||||
* Installs a load hook that bootstraps Lua when a page finished loading.
|
||||
*/
|
||||
public void install() {
|
||||
public void install(FxVideoHost videoHost, FxImageHost imageHost) {
|
||||
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
|
||||
if (newState == Worker.State.SUCCEEDED) {
|
||||
bootstrapped.set(false);
|
||||
@@ -87,8 +84,10 @@ public final class FxLuaScriptEngine implements AutoCloseable {
|
||||
FxUiHost uiHost = new FxUiHost(engine, dom);
|
||||
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
|
||||
FxEventHost eventHost = new FxEventHost(dom);
|
||||
// TODO: Default implementation or parameter for "audioHost"
|
||||
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost);
|
||||
FxAudioHost audioHost = new FxAudioHost();
|
||||
FxVideoHost videoHost = new FxVideoHost(engine, dom);
|
||||
FxImageHost imageHost = new FxImageHost(engine, dom);
|
||||
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost, imageHost, videoHost);
|
||||
LuaRuntime rt = new LuaRuntime(globals, services, policy);
|
||||
eventHost.setRouter(rt.eventRouter());
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
|
||||
import org.openautonomousconnection.luascript.tables.DomTable;
|
||||
import org.openautonomousconnection.luascript.tables.EventsTable;
|
||||
import org.openautonomousconnection.luascript.tables.UiTable;
|
||||
import org.openautonomousconnection.luascript.tables.*;
|
||||
import org.openautonomousconnection.luascript.tables.console.ConsoleTable;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -51,6 +49,9 @@ public final class LuaRuntime implements AutoCloseable {
|
||||
new ConsoleTable().inject(globals, services, overwrite);
|
||||
new EventsTable(dispatcher).inject(globals, services, overwrite);
|
||||
new DomTable().inject(globals, services, overwrite);
|
||||
new AudioTable().inject(globals, services, overwrite);
|
||||
new VideoTable().inject(globals, services, overwrite);
|
||||
new ImageTable().inject(globals, services, overwrite);
|
||||
}
|
||||
|
||||
public void bootstrapFromDom() {
|
||||
|
||||
@@ -2,28 +2,90 @@ package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.luaj.vm2.lib.ZeroArgFunction;
|
||||
import org.openautonomousconnection.luascript.hosts.AudioHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class AudioTable extends ScriptTable {
|
||||
protected AudioTable() {
|
||||
/**
|
||||
* Lua table: audio
|
||||
*
|
||||
* <p>Functions:</p>
|
||||
* <ul>
|
||||
* <li>audio.play(pathOrUrl)</li>
|
||||
* <li>audio.pause()</li>
|
||||
* <li>audio.stop()</li>
|
||||
* <li>audio.volume(v) (0..1)</li>
|
||||
* <li>audio.loop(boolean)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class AudioTable extends ScriptTable {
|
||||
|
||||
public AudioTable() {
|
||||
super("audio");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
AudioHost audioHost = services.audio().orElseThrow(() -> new IllegalStateException("AudioHost not provided"));
|
||||
AudioHost audioHost = services.audio()
|
||||
.orElseThrow(() -> new IllegalStateException("AudioHost not provided"));
|
||||
|
||||
table().set("play", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
String fileName = arg.checkjstring();
|
||||
audioHost.play(new File(fileName));
|
||||
String s = arg.checkjstring();
|
||||
|
||||
if (looksLikeUrl(s)) {
|
||||
audioHost.playUrl(s);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
|
||||
audioHost.playFile(new File(s));
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("pause", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
audioHost.pause();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("stop", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
audioHost.stop();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("volume", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
double v = arg.checkdouble();
|
||||
if (v < 0.0) v = 0.0;
|
||||
if (v > 1.0) v = 1.0;
|
||||
audioHost.setVolume(v);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("loop", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
audioHost.setLoop(arg.checkboolean());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean looksLikeUrl(String s) {
|
||||
if (s == null) return false;
|
||||
int idx = s.indexOf("://");
|
||||
return idx > 0 && idx < 16;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.luaj.vm2.lib.ThreeArgFunction;
|
||||
import org.luaj.vm2.lib.TwoArgFunction;
|
||||
import org.luaj.vm2.lib.ZeroArgFunction;
|
||||
import org.luaj.vm2.*;
|
||||
import org.luaj.vm2.lib.*;
|
||||
import org.openautonomousconnection.luascript.events.JavaToLua;
|
||||
import org.openautonomousconnection.luascript.hosts.DomHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
@@ -35,6 +32,20 @@ public final class DomTable extends ScriptTable {
|
||||
return t;
|
||||
}
|
||||
|
||||
private static Object luaToJavaScalar(LuaValue v) {
|
||||
if (v == null || v.isnil()) return null;
|
||||
if (v.isboolean()) return v.toboolean();
|
||||
if (v.isnumber()) {
|
||||
// Preserve integer when possible; otherwise double.
|
||||
if (v.isint()) return v.toint();
|
||||
if (v.islong()) return v.tolong();
|
||||
return v.todouble();
|
||||
}
|
||||
if (v.isstring()) return v.tojstring();
|
||||
// Reject complex types for safety (no silent assumptions).
|
||||
throw new LuaError("Unsupported value type for dom.setProp/call argument: " + v.typename());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
|
||||
@@ -125,8 +136,7 @@ public final class DomTable extends ScriptTable {
|
||||
table().set("children", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
List<String> children = dom.getChildrenIds(id.checkjstring());
|
||||
return toLuaArray(children);
|
||||
return toLuaArray(dom.getChildrenIds(id.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,5 +188,38 @@ public final class DomTable extends ScriptTable {
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setProp", new ThreeArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue prop, LuaValue value) {
|
||||
dom.setProperty(id.checkjstring(), prop.checkjstring(), luaToJavaScalar(value));
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getProp", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue prop) {
|
||||
Object ret = dom.getProperty(id.checkjstring(), prop.checkjstring());
|
||||
return JavaToLua.coerce(ret);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("call", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
String id = args.arg(1).checkjstring();
|
||||
String method = args.arg(2).checkjstring();
|
||||
|
||||
int n = args.narg();
|
||||
Object[] argv = new Object[Math.max(0, n - 2)];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
argv[i - 3] = luaToJavaScalar(args.arg(i));
|
||||
}
|
||||
|
||||
Object ret = dom.call(id, method, argv);
|
||||
return JavaToLua.coerce(ret);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
import org.luaj.vm2.lib.VarArgFunction;
|
||||
import org.luaj.vm2.lib.ZeroArgFunction;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.hosts.ImageHost;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Lua table: image
|
||||
*
|
||||
* <p>Functions:</p>
|
||||
* <ul>
|
||||
* <li>image.show(pathOrUrl)</li>
|
||||
* <li>image.hide()</li>
|
||||
* <li>image.rect(x, y, w, h)</li>
|
||||
* <li>image.opacity(v)</li>
|
||||
* <li>image.preserveRatio(boolean)</li>
|
||||
* <li>image.smooth(boolean)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class ImageTable extends ScriptTable {
|
||||
|
||||
public ImageTable() {
|
||||
super("image");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
ImageHost host = services.image()
|
||||
.orElseThrow(() -> new IllegalStateException("ImageHost not provided"));
|
||||
|
||||
table().set("show", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
String src = args.arg(1).checkjstring();
|
||||
if (looksLikeUrl(src)) host.showUrl(src);
|
||||
else host.showFile(new File(src));
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("hide", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
host.hide();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("rect", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
double x = args.arg(1).checkdouble();
|
||||
double y = args.arg(2).checkdouble();
|
||||
double w = args.arg(3).checkdouble();
|
||||
double h = args.arg(4).checkdouble();
|
||||
host.setRect(x, y, w, h);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("opacity", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
double o = args.arg(1).checkdouble();
|
||||
host.setOpacity(o);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("preserveRatio", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
boolean b = args.arg(1).checkboolean();
|
||||
host.setPreserveRatio(b);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("smooth", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
boolean b = args.arg(1).checkboolean();
|
||||
host.setSmooth(b);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean looksLikeUrl(String s) {
|
||||
if (s == null) return false;
|
||||
int idx = s.indexOf("://");
|
||||
return idx > 0 && idx < 16;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
import org.luaj.vm2.lib.VarArgFunction;
|
||||
import org.luaj.vm2.lib.ZeroArgFunction;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.hosts.VideoHost;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Lua table: video
|
||||
*
|
||||
* <p>Functions:</p>
|
||||
* <ul>
|
||||
* <li>video.play(pathOrUrl)</li>
|
||||
* <li>video.pause()</li>
|
||||
* <li>video.resume()</li>
|
||||
* <li>video.stop()</li>
|
||||
* <li>video.hide()</li>
|
||||
* <li>video.rect(x, y, w, h)</li>
|
||||
* <li>video.volume(v)</li>
|
||||
* <li>video.loop(boolean)</li>
|
||||
* <li>video.seek(seconds)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class VideoTable extends ScriptTable {
|
||||
|
||||
public VideoTable() {
|
||||
super("video");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
VideoHost host = services.video()
|
||||
.orElseThrow(() -> new IllegalStateException("VideoHost not provided"));
|
||||
|
||||
table().set("play", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
String src = args.arg(1).checkjstring();
|
||||
if (looksLikeUrl(src)) host.playUrl(src);
|
||||
else host.playFile(new File(src));
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("pause", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
host.pause();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("resume", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
host.resume();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("stop", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
host.stop();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("hide", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
host.hide();
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("rect", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
double x = args.arg(1).checkdouble();
|
||||
double y = args.arg(2).checkdouble();
|
||||
double w = args.arg(3).checkdouble();
|
||||
double h = args.arg(4).checkdouble();
|
||||
host.setRect(x, y, w, h);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("volume", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
double v = args.arg(1).checkdouble();
|
||||
host.setVolume(v);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("loop", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
boolean b = args.arg(1).checkboolean();
|
||||
host.setLoop(b);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("seek", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
double s = args.arg(1).checkdouble();
|
||||
host.seek(s);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean looksLikeUrl(String s) {
|
||||
if (s == null) return false;
|
||||
int idx = s.indexOf("://");
|
||||
return idx > 0 && idx < 16;
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,27 @@ package org.openautonomousconnection.luascript.tables.console;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.openautonomousconnection.luascript.hosts.ConsoleHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
public class ConsoleLogTable extends ScriptTable {
|
||||
/**
|
||||
* Creates a new script table with the given global name.
|
||||
/**
|
||||
* Lua table: console.log
|
||||
*/
|
||||
public final class ConsoleLogTable extends ScriptTable {
|
||||
|
||||
public ConsoleLogTable() {
|
||||
super("log");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
ConsoleHost console = services.console().orElseThrow(() -> new IllegalStateException("ConsoleHost not provided"));
|
||||
|
||||
table().set("info", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().info(arg.isnil() ? "nil" : arg.tojstring());
|
||||
console.info(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
@@ -26,7 +30,7 @@ public class ConsoleLogTable extends ScriptTable {
|
||||
table().set("log", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().log(arg.isnil() ? "nil" : arg.tojstring());
|
||||
console.log(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
@@ -34,7 +38,7 @@ public class ConsoleLogTable extends ScriptTable {
|
||||
table().set("warn", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().warn(arg.isnil() ? "nil" : arg.tojstring());
|
||||
console.warn(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,23 +2,27 @@ package org.openautonomousconnection.luascript.tables.console;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.openautonomousconnection.luascript.hosts.ConsoleHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
public class ConsoleStacktraceTable extends ScriptTable {
|
||||
/**
|
||||
* Creates a new script table with the given global name.
|
||||
/**
|
||||
* Lua table: console.stacktrace
|
||||
*/
|
||||
public final class ConsoleStacktraceTable extends ScriptTable {
|
||||
|
||||
public ConsoleStacktraceTable() {
|
||||
super("stacktrace");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
ConsoleHost console = services.console().orElseThrow(() -> new IllegalStateException("ConsoleHost not provided"));
|
||||
|
||||
table().set("print", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().error(arg.isnil() ? "nil" : arg.tojstring());
|
||||
console.error(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
@@ -26,7 +30,7 @@ public class ConsoleStacktraceTable extends ScriptTable {
|
||||
table().set("exception", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().exception(arg.isnil() ? "nil" : arg.tojstring());
|
||||
console.exception(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,10 +3,11 @@ package org.openautonomousconnection.luascript.tables.console;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
public class ConsoleTable extends ScriptTable {
|
||||
/**
|
||||
* Creates a new script table with the given global name.
|
||||
/**
|
||||
* Lua table: console
|
||||
*/
|
||||
public final class ConsoleTable extends ScriptTable {
|
||||
|
||||
public ConsoleTable() {
|
||||
super("console");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.openautonomousconnection.luascript.values;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
public class LuaObject extends LuaValue {
|
||||
@Override
|
||||
public int type() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String typename() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user