diff --git a/src/main/java/com/osiris/desku/App.java b/src/main/java/com/osiris/desku/App.java index 15974f0..efb1048 100644 --- a/src/main/java/com/osiris/desku/App.java +++ b/src/main/java/com/osiris/desku/App.java @@ -74,6 +74,14 @@ public class App { public static File styles; public static File javascript; + /** + * Make sure {@link LoggerParams} has debugging enabled for this to work.
+ * If this is enabled the debug output will include a much more detailed output + * related to the html that is added, the attributes being set etc.
+ * This also adds similar logging to the browsers console output.
+ */ + public static boolean isInDepthDebugging = false; + static { updateDirs(); } @@ -116,7 +124,7 @@ public static void updateDirs(){ public static class LoggerParams{ public String name = "Logger"; - public boolean debug = true; + public boolean debug = false; public File logsDir; public File latestLogFile; public File mirrorOutFile; @@ -160,12 +168,12 @@ public static void init(UIManager uiManager, LoggerParams loggerParams) { AL.start(loggerParams.name, loggerParams.debug, loggerParams.latestLogFile, loggerParams.ansi, loggerParams.forceAnsi); AL.mirrorSystemStreams(loggerParams.mirrorOutFile, loggerParams.mirrorErrFile); } - AL.info("Starting application..."); - AL.info("workingDir = " + workingDir); - AL.info("tempDir = " + tempDir); - AL.info("userDir = " + userDir); - AL.info("htmlDir = " + htmlDir); - AL.info("Java = " + System.getProperty("java.vendor") + " " + System.getProperty("java.version")); + AL.debug(App.class, "Starting application..."); + AL.debug(App.class, "workingDir = " + workingDir); + AL.debug(App.class, "tempDir = " + tempDir); + AL.debug(App.class, "userDir = " + userDir); + AL.debug(App.class, "htmlDir = " + htmlDir); + AL.debug(App.class, "Java = " + System.getProperty("java.vendor") + " " + System.getProperty("java.version")); // Clear the directory at each app startup, since // its aim is to provide a cache to load pages faster @@ -187,7 +195,7 @@ public static void init(UIManager uiManager, LoggerParams loggerParams) { appendToGlobalCSS(getCSS(Bootstrap.class)); appendToGlobalJS(getJS(Bootstrap.class)); - AL.info("Started application successfully!"); + AL.debug(App.class, "Started application successfully!"); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/osiris/desku/ui/Component.java b/src/main/java/com/osiris/desku/ui/Component.java index e2d7022..0ad6f2a 100644 --- a/src/main/java/com/osiris/desku/ui/Component.java +++ b/src/main/java/com/osiris/desku/ui/Component.java @@ -22,6 +22,8 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -61,6 +63,11 @@ public class Component, VALUE> { * */ public final int id = idCounter.getAndIncrement(); + /** + * List of children. Normally it's read-only.
+ * Thus do not modfify directly and use methods like {@link #add(Component[])} or {@link #remove(Component[])} + * instead, to ensure the changes are also visible in the browser. + */ public final CopyOnWriteArrayList children = new CopyOnWriteArrayList<>(); /** * Executed when a child was added on the Java side.
@@ -149,17 +156,17 @@ public boolean isAttached(){ child.element.remove(); child.update(); - // Update UI - if (isAttached && !ui.isLoading()){ - ui.executeJavaScriptSafely(ui.jsGetComp("comp", id) + - ui.jsGetComp("childComp", child.id) + - "comp.removeChild(childComp);\n", - "internal", 0); - } - - child.isAttached = false; - onChildRemove.execute(child); } + UI.PendingAppend.removeFromPendingAppends(ui, child); + + // Update UI always + // Child might have already been removed in Java but not in JS + executeJS(ui.jsGetComp("childComp", child.id) + + "comp.removeChild(childComp);\n" + + (App.isInDepthDebugging ? "console.log('parent comp:', comp); console.log('➡️❌ removed childComp:', childComp); \n" : "")); + + child.isAttached = false; + onChildRemove.execute(child); }; public Consumer _removeSelf = self -> { UI ui = UI.get(); // Necessary for updating the actual UI via JavaScript @@ -167,13 +174,12 @@ public boolean isAttached(){ self.element.remove(); } self.update(); + UI.PendingAppend.removeFromPendingAppends(ui, self); // Update UI - if (isAttached && !ui.isLoading()){ - ui.executeJavaScriptSafely(ui.jsGetComp("comp", self.id) + - "comp.parentNode.removeChild(comp);\n", - "internal", 0); - } + executeJS(ui.jsGetComp("comp", self.id) + + "comp.parentNode.removeChild(comp);\n"+ + (App.isInDepthDebugging ? "console.log('parent comp:', comp.parentNode); console.log('➡️❌ removed self:', comp); \n" : "")); self.isAttached = false; //onChildRemove.execute(self); @@ -191,6 +197,8 @@ public boolean isAttached(){ e.childComp.update(); element.insertChildren(iOtherComp, e.childComp.element); } else if (e.isReplace) { + // childComp is the new component to be added + // and otherChildComp is the one that gets removed/replaced int iOtherComp = children.indexOf(e.otherChildComp); children.set(iOtherComp, e.childComp); e.childComp.update(); @@ -263,7 +271,9 @@ public boolean isAttached(){ executeJS("comp.setAttribute(`" + key + "`, `" + value + "`);\n" + "comp[`"+key+"`] = `"+value+"`\n"); // Change UI representation - //System.out.println(key+" = "+ value); + if(App.isInDepthDebugging) AL.debug(this.getClass(), this.toPrintString()+" _attributeChange javascript -> "+key+" = "+ value); + } else { + if(App.isInDepthDebugging) AL.debug(this.getClass(), this.toPrintString()+" _attributeChange Java -> "+key+" = "+ value); } } else {// Remove attribute @@ -334,11 +344,15 @@ public Component(@UnknownNullability VALUE value, @NotNull Class valueCla * that was set in the constructor. */ public THIS getValue(Consumer<@NotNull VALUE> v) { + UI ui = UI.get(); - if(!isAttached || ui == null || ui.isLoading()) // Since never attached once, user didn't have a chance to change the value, thus return internal directly + if(!isAttached || ui == null || ui.isLoading()) { // Since never attached once, user didn't have a chance to change the value, thus return internal directly v.accept(internalValue); + if(App.isInDepthDebugging) AL.debug(this.getClass(), this.toPrintString()+" getValue() returns internalValue = "+ internalValue); + } else gatr("value", valueAsString -> { + if(App.isInDepthDebugging) AL.debug(this.getClass(), this.toPrintString()+" getValue() returns from javascript value attribute = "+valueAsString); VALUE value = Value.stringToVal(valueAsString, this); v.accept(value); }); @@ -359,6 +373,8 @@ public THIS setValue(@Nullable VALUE v) { else newValJsonSafe = "\""+Value.escapeForJSON(newVal)+"\""; // json object or other primitive String message = "{\"newValue\": "+newValJsonSafe+"}"; + if(App.isInDepthDebugging) AL.debug(this.getClass(), this.toPrintString()+" setValue() message -> "+message); + JsonObject jsonEl = JsonFile.parser.fromJson(message, JsonObject.class); ValueChangeEvent event = new ValueChangeEvent<>(message, jsonEl, _this, v, this.internalValue, true); this.internalValue = v; @@ -411,8 +427,16 @@ public boolean isValuesEqual(VALUE val1, VALUE val2){ /** * Executes the provided JavaScript code now, or later * if this component is not attached yet.
+ *
+ * Does nothing and directly returns if the UI is null or still loading, since + * we assume that your provided JS code is strongly related to this component and that + * it does a manipulation that was done in Java code before (like for example changing its HTML, a style or attribute) + * to prevent duplicate operations.
+ * If that is not the case use {@link UI#executeJavaScriptSafely(String, String, int)} instead.
+ *
* Your code will be encapsulated in a try/catch block and errors logged to * the clients JavaScript console.
+ *
* A reference of this component will be added before your code, thus you can access * this component via the "comp" variable in your provided JavaScript code. */ @@ -424,6 +448,7 @@ public THIS executeJS(String code){ * @see #executeJS(String) */ public THIS executeJS(UI ui, String code){ + if(ui == null || ui.isLoading()) return _this; if(isAttached){ ui.executeJavaScriptSafely( "try{"+ @@ -475,14 +500,17 @@ public THIS now(Consumer code) { } /** - * Executes the provided code asynchronously in a new thread.
+ * Executes the provided code asynchronously in a thread from {@link App#executor} and returns directly.
* This function needs to be run inside UI context * since it executes {@link UI#get()}, otherwise {@link NullPointerException} is thrown.
*
* Note that your code-block will have access to the current UI, * which means that you can add/remove/change UI components without issues. * This also means that you will have to handle Thread-safety yourself - * when doing things to the same component from multiple threads at the same time. + * when doing things to the same component from multiple threads at the same time.
+ *
+ * Also note that your code is not allowed to run forever/block, since + * sometimes added components get added (in the browser) only after your code was run, see {@link UI#pendingAppends} and {@link UI#access(Runnable)}. * * @param code the code to be executed asynchronously, contains this component as parameter. */ @@ -837,6 +865,26 @@ public THIS visible(boolean b) { return _this; } + /** + * Requires that the children have absolute width/height.
+ * If this component has width/height set, those are used, otherwise 100% as width/height is used. + */ + public THIS scrollable(boolean b) { + String width = style.get("width").isEmpty() ? "100%" : style.get("width"); + String height = style.get("height").isEmpty() ? "100%" : style.get("height"); + scrollable(b, width, height, "", ""); + return _this; + } + + /** + * Requires that the children have absolute width/height. + */ + public THIS scrollable(boolean b, String width, String height) { + scrollable(b, width, height, "", ""); + return _this; + } + + /** * Makes this component scrollable.
* Note that you must also set the width and height for this to work,
diff --git a/src/main/java/com/osiris/desku/ui/HTTPServer.java b/src/main/java/com/osiris/desku/ui/HTTPServer.java index 8e8bcfb..c41c0e1 100644 --- a/src/main/java/com/osiris/desku/ui/HTTPServer.java +++ b/src/main/java/com/osiris/desku/ui/HTTPServer.java @@ -57,7 +57,7 @@ public Response serve(IHTTPSession session) { return sendHTMLString(msg + "\n"); } try { - AL.info("File: " + f); + AL.debug(this.getClass(), "File: " + f); return sendFile(f); } catch (Exception e) { String err = "Failed to provide content for " + path + " due to an exception '" + e.getMessage() + "' (more details in the log). File: " + f; diff --git a/src/main/java/com/osiris/desku/ui/UI.java b/src/main/java/com/osiris/desku/ui/UI.java index a47dd11..d225ed7 100644 --- a/src/main/java/com/osiris/desku/ui/UI.java +++ b/src/main/java/com/osiris/desku/ui/UI.java @@ -2,6 +2,7 @@ import com.osiris.desku.App; import com.osiris.desku.Route; +import com.osiris.desku.Value; import com.osiris.desku.ui.utils.Rectangle; import com.osiris.desku.ui.utils.UnsafePortChrome; import com.osiris.events.Event; @@ -34,7 +35,13 @@ public abstract class UI { * Boolean parameter isLoading, is true if still loading or false if finished loading. */ public final Event onLoadStateChanged = new Event<>(); - private final List pendingAppends = new ArrayList<>(); + /** + * This allows the programmer to add components to a parent even if the parent is + * not attached yet (out of order additions). However those additions will only be visible in Java + * and later in the browser.
+ * Access using "synchronized (pendingAppends)" to ensure order. + */ + public final List pendingAppends = new ArrayList<>(); /** * Last loaded html. */ @@ -142,7 +149,7 @@ public static void remove(Thread... threads) { public abstract void plusY(int y) throws InterruptedException, InvocationTargetException; /** - * Executes {@link #executeJavaScript(String, String, int)} (String, String, int)} only once the UI is loaded and after + * Executes {@link #executeJavaScript(String, String, int)} only once the UI is loaded and after * some internals JS dependencies are loaded. * * @see #getSnapshot() internal JS dependencies are added here. @@ -239,7 +246,8 @@ public void reload() { } /** - * Access this window synchronously now. + * Access this window synchronously now and executes any {@link #pendingAppends} + * after running the provided code. */ public UI access(Runnable code) { UI.set(this, Thread.currentThread()); @@ -261,8 +269,8 @@ public UI access(Runnable code) { public void safeInit(String startURL, boolean isTransparent, boolean isDecorated, int widthPercent, int heightPercent) { try { - AL.info("Starting new UI/window with url: " + startURL + " transparent: " + isTransparent + " width: " + widthPercent + "% height: " + heightPercent + "%"); - AL.info("Waiting for it to finish loading... Please stand by..."); + AL.debug(this.getClass(), "Starting new UI/window with url: " + startURL + " transparent: " + isTransparent + " width: " + widthPercent + "% height: " + heightPercent + "%"); + AL.debug(this.getClass(), "Waiting for it to finish loading... Please stand by..."); long ms = System.currentTimeMillis(); /** @@ -283,7 +291,7 @@ public void safeInit(String startURL, boolean isTransparent, boolean isDecorated while (isLoading.get()) Thread.yield(); - AL.info("Init took " + (System.currentTimeMillis() - ms) + "ms for " + this); + AL.debug(this.getClass(), "Init took " + (System.currentTimeMillis() - ms) + "ms for " + this); } catch (Exception e) { throw new RuntimeException(e); } @@ -293,15 +301,15 @@ public void close() { App.uis.all.remove(this); try { webSocketServer.stop(); - AL.info("Closed WebSocketServer " + webSocketServer.domain + ":" + webSocketServer.port + " for UI: " + this); + AL.debug(this.getClass(), "Closed WebSocketServer " + webSocketServer.domain + ":" + webSocketServer.port + " for UI: " + this); } catch (Exception e) { } try { httpServer.server.stop(); - AL.info("Closed HTTPServer " + httpServer.serverDomain + ":" + httpServer.serverPort + " for UI: " + this); + AL.debug(this.getClass(), "Closed HTTPServer " + httpServer.serverDomain + ":" + httpServer.serverPort + " for UI: " + this); } catch (Exception e) { } - AL.info("Closed " + this); + AL.debug(this.getClass(), "Closed " + this); } /** @@ -414,7 +422,7 @@ public File snapshotToTempFile(Document snapshot) throws IOException { if (snapshot == null) snapshot = getSnapshot(); // Write html to temp file - AL.info("Generate: " + file); + AL.debug(this.getClass(), "Generate: " + file); file.getParentFile().mkdirs(); if (!file.exists()) file.createNewFile(); Files.write(file.toPath(), snapshot.outerHtml().getBytes(StandardCharsets.UTF_8)); @@ -423,7 +431,7 @@ public File snapshotToTempFile(Document snapshot) throws IOException { public File getSnapshotTempFile() { return new File(App.htmlDir - + (route.path.equals("/") || route.path.equals("") ? "/.html" : (route.path + ".html"))); + + (route.path.equals("/") || route.path.isEmpty() ? "/.html" : (route.path + ".html"))); } /** @@ -607,7 +615,7 @@ public void startHTTPServer() throws Exception { public synchronized void startHTTPServer(String serverDomain, int serverPort) throws Exception { httpServer = new HTTPServer(this, serverDomain, serverPort); serverPort = httpServer.serverPort; - AL.info("Started HTTPServer " + serverDomain + ":" + serverPort + " for UI: " + this); + AL.debug(this.getClass(), "Started HTTPServer " + serverDomain + ":" + serverPort + " for UI: " + this); } /** @@ -639,12 +647,12 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { super.onOpen(conn, handshake); // Executed when client connects, since its executed at the end of the HTML body // this tells us that the page is loaded for the first time too - AL.info(this + " init success!"); + AL.debug(this.getClass(), this + " init success!"); onLoadStateChanged.execute(false); } }; serverPort = webSocketServer.port; - AL.info("Started WebSocketServer " + serverDomain + ":" + serverPort + " for UI: " + this); + AL.debug(this.getClass(), "Started WebSocketServer " + serverDomain + ":" + serverPort + " for UI: " + this); } public String jsClientSendWebSocketMessage(String message) { @@ -665,7 +673,7 @@ public String jsGetComp(String varName, int id) { } /** - * Ensures all parents are attached before performing actually + * Ensures all parents are attached before actually * performing the pending append operation. */ private void attachToParentSafely(PendingAppend pendingAppend) { @@ -698,6 +706,12 @@ private void attachToParentSafely(PendingAppend pendingAppend) { } } + public List getPendingAppendsCopy(){ + synchronized (pendingAppends){ + return new ArrayList<>(pendingAppends); + } + } + public void attachWhenAccessEnds(Component parent, Component child, Component.AddedChildEvent e) { synchronized (pendingAppends) { pendingAppends.add(new PendingAppend(parent, child, e)); @@ -705,8 +719,8 @@ public void attachWhenAccessEnds(Component parent, Component child, } public > void attachToParent(Component parent, Component child, Component.AddedChildEvent e) { - //AL.info("attachToParent() "+parent.getClass().getSimpleName()+"("+parent.id+"/"+parent.isAttached()+") ++++ "+ - // child.getClass().getSimpleName()+"("+child.id+") "); + if(App.isInDepthDebugging) AL.debug(this.getClass(), "attachToParent() parent = "+parent.toPrintString()+" attached="+parent.isAttached()+" added child = "+ + child.toPrintString()+" child html = \n"+ child.element.outerHtml()); if (e.otherChildComp == null) { // add executeJavaScript(jsAttachToParent(parent, child), @@ -716,10 +730,13 @@ public void attachWhenAccessEnds(Component parent, Component child, child2.setAttached(true); }); } else if (e.isInsert || e.isReplace) { // for replace, remove() must be executed after this function returns + // if replace: childComp is the new component to be added and otherChildComp is the one that gets removed/replaced + // "beforebegin" = Before the element. Only valid if the element is in the DOM tree and has a parent element. executeJavaScript( jsGetComp("otherChildComp", e.otherChildComp.id) + - "var child = `" + e.childComp.element.outerHtml() + "`;\n" + - "otherChildComp.insertAdjacentHTML(\"beforebegin\", child);\n", + "var child = `" + Value.escapeForJavaScript(Value.escapeForJSON(e.childComp.element.outerHtml())) + "`;\n" + + "otherChildComp.insertAdjacentHTML(\"beforebegin\", child);\n" + + (App.isInDepthDebugging ? "console.log('otherChildComp:', otherChildComp); console.log('➡️✅ inserted child:', child); \n" : ""), "internal", 0); e.childComp.setAttached(true); e.childComp.forEachChildRecursive(child2 -> { @@ -730,10 +747,9 @@ public void attachWhenAccessEnds(Component parent, Component child, public String jsAttachToParent(Component parent, Component child) { return "try{" + jsGetComp("parentComp", parent.id) + - "var child = `\n" + child.element.outerHtml() + "\n`;\n" + + "var child = `\n" + Value.escapeForJavaScript(Value.escapeForJSON(child.element.outerHtml())) + "\n`;\n" + "parentComp.insertAdjacentHTML(\"beforeend\", child);\n" + - //"console.log('ADDED CHILD: ');\n"+ - //"console.log(child);\n" + + (App.isInDepthDebugging ? "console.log('parentComp:', parentComp); console.log('➡️✅ added child:', child);\n" : "") + "\n}catch(e){console.error(e)}"; } @@ -748,13 +764,38 @@ public void runIfReadyOrLater(Runnable code) { } } + public void runIfReadyAndCompAttachedOrLater(Component comp, Runnable code) { + synchronized (isLoading){ + if (!isLoading.get() && comp.isAttached()) code.run(); + else onLoadStateChanged.addAction((action, isLoading) -> { + if (isLoading && !comp.isAttached()) return; + action.remove(); + code.run(); + }, AL::warn); + } + } + public boolean isLoading(){ synchronized (isLoading){ return isLoading.get(); } } - private class PendingAppend { + public static class PendingAppend { + public static boolean removeFromPendingAppends(UI ui, Component comp){ + if(ui != null){ // && ui.isLoading()){ // Also remove from pending appends, these can also happen after loading + synchronized (ui.pendingAppends){ + List toRemove = new ArrayList<>(0); + for (UI.PendingAppend pendingAppend : ui.pendingAppends) { + if(comp.equals(pendingAppend.child)) + toRemove.add(pendingAppend); + } + return ui.pendingAppends.removeAll(toRemove); + } + } + return false; + } + public Component parent; public Component child; public Component.AddedChildEvent e; diff --git a/src/main/java/com/osiris/desku/ui/display/Image.java b/src/main/java/com/osiris/desku/ui/display/Image.java index 47dd3db..9de19e1 100644 --- a/src/main/java/com/osiris/desku/ui/display/Image.java +++ b/src/main/java/com/osiris/desku/ui/display/Image.java @@ -44,7 +44,7 @@ public Image(RenderedImage image, String name) { String format = name.substring(name.lastIndexOf(".") + 1); if(!ImageIO.write(image, format, imgFile)) throw new Exception("No writer for image of type \""+format+"\"."); - AL.info("Written java-image to: " + imgFile); + AL.debug(this.getClass(), "Written java-image to: " + imgFile); } catch (Exception e) { AL.warn("Failed to write java-image ("+imgFile+").", e); } @@ -89,7 +89,7 @@ public Image(String packagePath, String src) { img.getParentFile().mkdirs(); try { Files.copy(App.getResource(packagePath + src), img.toPath()); - //AL.info("Unpacked image to: " + img); + //AL.debug(this.getClass(), "Unpacked image to: " + img); } catch (Exception e) { AL.warn(e); } diff --git a/src/main/java/com/osiris/desku/ui/display/Text.java b/src/main/java/com/osiris/desku/ui/display/Text.java index 1f9aced..37624c8 100644 --- a/src/main/java/com/osiris/desku/ui/display/Text.java +++ b/src/main/java/com/osiris/desku/ui/display/Text.java @@ -23,15 +23,12 @@ public Text(String s) { UI ui = UI.get(); Runnable registration = () -> { _onValueAppended.addAction((childString) -> { - ui.executeJavaScriptSafely(ui.jsGetComp("comp", id) + - "var childString = document.createTextNode(`" + childString + "`);\n" + - "comp.appendChild(childString);\n", - "internal", 0); + executeJS("var childString = document.createTextNode(`" + childString + "`);\n" + + "comp.appendChild(childString);\n"); }); _onEmptyValue.addAction((_void) -> { - ui.executeJavaScriptSafely(ui.jsGetComp("comp", id) + - "comp.textContent = '';\n", // remove all text nodes - "internal", 0); + executeJS("comp.textContent = '';\n"); // remove all text nodes + removeAll(); }); }; ui.runIfReadyOrLater(registration); diff --git a/src/main/java/com/osiris/desku/ui/input/filechooser/FileChooser.java b/src/main/java/com/osiris/desku/ui/input/filechooser/FileChooser.java index 2ab62fc..5ae24e1 100644 --- a/src/main/java/com/osiris/desku/ui/input/filechooser/FileChooser.java +++ b/src/main/java/com/osiris/desku/ui/input/filechooser/FileChooser.java @@ -13,6 +13,7 @@ import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -59,26 +60,23 @@ public static String pathsListToString(List list) { public static List stringToPathsList(String s) { List l = new ArrayList<>(); for (String path : s.split(";")) { - l.add(new File(path.trim())); + path = path.trim(); + if(!path.isEmpty()) l.add(new File(path)); } return l; } public FileChooser(Text label, String defaultValue) { super(defaultValue, String.class); - this.label = label; - this.tfSelectedFiles = new TextField(label, defaultValue); - this.directoryView = new DirectoryView(this, App.userDir.getAbsoluteFile()); - this.btnsSelectedFiles = new Horizontal().padding(false); - btnsSelectedFiles.add(new Button("Select File(s)").onClick(e -> { - directoryView.visible(!directoryView.isVisible()); - })); - for (File file : stringToPathsList(defaultValue)) { - btnsSelectedFiles.add(getButton(new FileAsRow(this, directoryView, file))); - } - tfSelectedFiles.visible(false); + this.childVertical().childGap(true); + add(this.label = label); + add(this.tfSelectedFiles = new TextField(label, defaultValue)); + add(this.btnsSelectedFiles = new Horizontal().padding(false).scrollable(true, "100%", "fit-content")); + add(this.directoryView = new DirectoryView(this, App.userDir.getAbsoluteFile())); + setValue(defaultValue); + directoryView.visible(false); - childVertical(); + tfSelectedFiles.visible(false); tfSelectedFiles.onClick(e -> { directoryView.visible(!directoryView.isVisible()); }); @@ -98,9 +96,16 @@ public FileChooser(Text label, String defaultValue) { btnsSelectedFiles.remove(btn); } }); + } - this.childGap(true); - add(this.label, this.btnsSelectedFiles, this.tfSelectedFiles, this.directoryView); + private void setButtons(List files){ + btnsSelectedFiles.removeAll(); + btnsSelectedFiles.add(new Button("Select File(s)").onClick(e -> { + directoryView.visible(!directoryView.isVisible()); + })); + for (File file : files) { + btnsSelectedFiles.add(getButton(new FileAsRow(this, directoryView, file))); + } } private Button getButton(FileAsRow e) { @@ -109,9 +114,22 @@ private Button getButton(FileAsRow e) { }); } + public FileChooser setValue(@Nullable File... v) { + if(v == null) setValue(""); + else setValue(pathsListToString(Arrays.asList(v))); + return this; + } + + public FileChooser setValue(@Nullable List v) { + if(v == null) setValue(""); + else setValue(pathsListToString(v)); + return this; + } + @Override public FileChooser setValue(@Nullable String v) { tfSelectedFiles.setValue(v); + setButtons(stringToPathsList(v)); return this; } diff --git a/src/test/java/com/osiris/desku/TApp.java b/src/test/java/com/osiris/desku/TApp.java index 55446a6..139edc2 100644 --- a/src/test/java/com/osiris/desku/TApp.java +++ b/src/test/java/com/osiris/desku/TApp.java @@ -3,7 +3,10 @@ import com.osiris.desku.ui.Component; import com.osiris.desku.ui.DesktopUIManager; import com.osiris.desku.ui.UI; +import com.osiris.desku.ui.UIManager; +import com.osiris.jlib.logger.AL; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -12,7 +15,6 @@ * Test App used globally for all tests. */ public class TApp { - public static MRoute route; public static UI ui; public static class AsyncResult{ @@ -20,12 +22,18 @@ public static class AsyncResult{ public AtomicReference refEx = new AtomicReference<>(); } + public static CopyOnWriteArrayList uiManagers = new CopyOnWriteArrayList<>(); + public static AsyncResult testAndAwaitResult(Function> onLoad){ AsyncResult asyncResult = new AsyncResult(); - App.init(new DesktopUIManager()); - route = new MRoute("/", () -> onLoad.apply(asyncResult)); + App.isInDepthDebugging = true; + var logger = new App.LoggerParams(); + logger.debug = true; + var uiManager = new DesktopUIManager(); + uiManagers.add(uiManager); + App.init(uiManager, logger); try { - ui = App.uis.create(route); + ui = uiManager.create(() -> onLoad.apply(asyncResult)); while(ui.isLoading()) Thread.yield(); for (int i = 0; i < 30; i++) { @@ -35,6 +43,13 @@ public static AsyncResult testAndAwaitResult(Function> onLoad){ AsyncResult asyncResult = new AsyncResult(); - App.init(new DesktopUIManager()); - route = new MRoute("/", () -> onLoad.apply(asyncResult)); + App.isInDepthDebugging = true; + var logger = new App.LoggerParams(); + logger.debug = true; + App.init(new DesktopUIManager(), logger); try { - ui = App.uis.create(route); + ui = App.uis.create(() -> onLoad.apply(asyncResult)); while(ui.isLoading()) Thread.yield(); while(true) diff --git a/src/test/java/com/osiris/desku/bugs/FileChooserTest.java b/src/test/java/com/osiris/desku/bugs/FileChooserTest.java index 06ce160..bbfe2bc 100644 --- a/src/test/java/com/osiris/desku/bugs/FileChooserTest.java +++ b/src/test/java/com/osiris/desku/bugs/FileChooserTest.java @@ -37,7 +37,7 @@ void test() throws Throwable { return new Vertical() .add(c) - .later(v -> { + .later(ly -> { try{ while(UI.get().isLoading()) Thread.yield(); // Wait to ensure not the internal value is directly returned // but instead the value is returned from the frontend HTML value attribute of the component. @@ -56,10 +56,10 @@ void test() throws Throwable { if(defaultFiles.isEmpty()) defaultFiles.add(App.workingDir.listFiles()[0]); FileChooser c2 = new FileChooser("FC2", defaultFiles); + ly.add(c2); assertEquals(FileChooser.pathsListToString(defaultFiles), c2.getValue()); - - + c2.setValue(new File(App.workingDir+"/test")); } catch (Throwable e) { asyncResult.refEx.set(e); } finally { diff --git a/src/test/java/com/osiris/desku/bugs/ReplacingTest.java b/src/test/java/com/osiris/desku/bugs/ReplacingTest.java new file mode 100644 index 0000000..e45dcc6 --- /dev/null +++ b/src/test/java/com/osiris/desku/bugs/ReplacingTest.java @@ -0,0 +1,46 @@ +package com.osiris.desku.bugs; + +import com.osiris.desku.TApp; +import com.osiris.desku.ui.Component; +import com.osiris.desku.ui.UI; +import com.osiris.desku.ui.input.TextField; +import com.osiris.desku.ui.layout.Vertical; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ReplacingTest { + + @Test + void test() throws Throwable { + TApp.testAndAwaitResult((asyncResult) -> { + Vertical ly = new Vertical(); + TextField tf1 = new TextField("TF1", ""); + TextField tf2 = new TextField("TF2", ""); + ly.add(tf1); + + ly.replace(tf1, tf2); + + assertEquals(tf2, ly.children.get(0)); + + return ly + .later(__ -> { + try{ + while(UI.get().isLoading()) Thread.yield(); // Wait to ensure not the internal value is directly returned + // but instead the value is returned from the frontend HTML value attribute of the component. + + ly.replace(tf2, tf1); + + assertEquals(tf1, ly.children.get(0)); + + } catch (Throwable e) { + asyncResult.refEx.set(e); + } finally { + asyncResult.isDone.set(true); + } + }); + }); + + } +}