Skip to content
Snippets Groups Projects
Commit 836833c9 authored by Jari-Matti Mäkelä's avatar Jari-Matti Mäkelä
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #29104 passed
Showing
with 669 additions and 0 deletions
contracts/
.classpath
.project
.history/
.idea
.jqwik-database
.lib/
.worksheet
.settings/
*.iml
*.ipr
*.iws
*.log
project/boot/
project/plugins/project/
project/project/
project/*-shim.sbt
project/target/
target/
openjfx/
build/
.gradle/
doc/
image: maven:latest
variables:
# This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
# -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
# To keep cache across branches add ''
cache:
key: "$CI_JOB_NAME"
paths:
- .m2/repository
build:
stage: build
script:
- mvn compile
test:
stage: test
script:
- mvn test
This diff is collapsed.
README.md 0 → 100644
# Project description
A simple hot reload system for editing live JavaFX applications without
restarting the JVM. Requires Java 11 or later. Compatible with
Eclipse and IntelliJ IDEA. Minor issues with Netbeans.
## Installation
Maven:
```bash
$ git clone https://gitlab.utu.fi/tech/education/gui/hotreload
$ cd hotreload
$ mvn install
```
## Using
First, add the library as a Maven dependency:
```xml
<project>
<properties>
<hotreload.version>1.0.0</hotreload.version>
</properties>
<dependencies>
<dependency>
<groupId>fi.utu.tech</groupId>
<artifactId>hotreload</artifactId>
<version>${hotreload.version}</version>
</dependency>
</dependencies>
</project>
```
### Hot reload
To support hot reloading, the main `Application` class should implement
the interface `ReloadableApplication`:
```java
public interface ReloadableApplication {
SceneLoader getSceneLoader();
default List<String> createStyles() {
return List.of();
}
default boolean useGradle() {
...
}
default boolean useMaven() {
...
}
default void setScene(Stage stage, SceneLoader loader) {
...
}
default void setStyleSheet(Stage stage) {
...
}
}
```
Here one only needs to modify `useGradle()` and `useMaven()`
when using a non-standard project layout / multi-module project.
The `setScene` and `setStyleSheet` implementations should also work
by default.
The `createStyles()` routine should return a list of URIs for
global application wide style sheets. These will be reloaded when
the style sheet files change or classes need to be reloaded.
The `getSceneLoader()` should return a scene loader, which
is defined by the following interface:
```java
public interface SceneLoader {
Parent root();
default Controller controller() { return null; }
}
```
One should define a main scene loader using this interface. The `root()`
method should return a `Parent` object, e.g. some `Pane`. The basic idea
is that a new `SceneLoader` implementation will be instantiated each
time the application is reloaded. This is then used to contruct the
rest of the GUI.
The `controller()` may also optionally return a new `Controller`
object, implementing the following interface:
```java
public interface Controller {
default void setStage(Stage s) {}
}
```
For instance, when initializing FXML forms, providing the stage like
this can be useful since the default `initialize()` routine does not
pass any values. Note that the implementation is optional.
Finally, in order to activate the hot reload, one should initiate
the class monitor in the `Application`'s `start()` method:
```java
public void start(final Stage stage) {
try {
HotReloadMonitor monitor = new HotReloadMonitor(
this, stage, "fi.utu.tech.hotreload.demo"
);
monitor.start();
} catch (Exception e) {
System.err.println(e.getMessage());
}
...
}
```
The monitor should be closed when the application is done, otherwise
background monitor threads will remain active.
In addition, the defined `setScene` and `getSceneLoader` methods
can be used to initialize the scene:
```java
public void start(final Stage stage) {
...
setScene(stage, getSceneLoader());
}
```
## Demo
<a class="no-attachment-icon" href="web/hotreload.mp4" target="_blank" rel="noopener noreferrer">
<img src="web/hotreload.jpg" class="js-lazy-loaded qa-js-lazy-loaded">
</a>
## APIDoc
Online docs: <https://ftdev.utu.fi/apidocs/hotreload/>
The project comes with a Doxygen configuration. Just run doxygen in the
project root folder:
```
$ doxygen
```
The maven script will also generate standard javadoc apidocs in target/apidocs.
## Further reading
* https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html
* https://www.journaldev.com/349/java-classloader
* https://en.wikipedia.org/wiki/Java_Classloader
Hot reloading:
* https://dcevm.github.io/
* http://hotswapagent.org/index.html
* https://github.com/bytetroll/JavaFXCSSHotReload
* https://github.com/TravaOpenJDK/trava-jdk-11-dcevm
* https://sites.google.com/a/athaydes.com/renato-athaydes/posts/4freewaystohot-swapcodeonthejvm
* https://sites.google.com/a/athaydes.com/renato-athaydes/posts/saynotoelectronusingjavafxtowriteafastresponsivedesktopapplication
plugins {
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.9'
}
mainClassName = 'fi.utu.tech.demo.Main'
sourceCompatibility = 11
javafx {
version = "14.0.1"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.graphics' ]
}
jar {
manifest {
attributes 'Main-Class': mainClassName
}
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
sourceSets {
main {
java {
srcDirs = ['src/main/java']
}
resources {
srcDirs = ['src/main/resources']
}
}
test {
java {
srcDirs = ['src/test/java']
}
resources {
srcDirs = ['src/test/resources']
}
}
}
repositories {
jcenter()
repositories {
maven {
url "https://ftdev.utu.fi/maven2"
}
}
}
dependencies {
implementation 'fi.utu.tech:hotreload:1.0.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.6.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.6.2'
testImplementation 'org.junit.platform:junit-platform-commons:1.6.2'
testImplementation 'net.jqwik:jqwik:1.3.0'
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>hotreload-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>Hot reloader demo</name>
<url>https://gitlab.utu.fi/tech/education/gui/hotreload</url>
<packaging>jar</packaging>
<parent>
<groupId>fi.utu.tech</groupId>
<artifactId>hotreload-meta</artifactId>
<version>1.0.0</version>
</parent>
<properties>
<project.mainclass>fi.utu.tech.demo.Main</project.mainclass>
<hotreload.version>1.0.0</hotreload.version>
</properties>
<dependencies>
<!-- JavaFX (remove if not needed to speed up dep downloads)-->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- UTU libraries -->
<dependency>
<groupId>fi.utu.tech</groupId>
<artifactId>hotreload</artifactId>
<version>${hotreload.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Run this app with exec:java -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<skip>false</skip>
<mainClass>${project.mainclass}</mainClass>
</configuration>
</plugin>
<!-- Make the packaged jar executable -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<!-- DO NOT include log4j.properties file in your Jar -->
<excludes>
<exclude>**/log4j.properties</exclude>
</excludes>
<archive>
<manifest>
<!-- Jar file entry point -->
<mainClass>${project.mainclass}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
package fi.utu.tech.demo;
import fi.utu.tech.hotreload.Controller;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class BoardController implements Controller {
@FXML
private Label status;
@FXML
private ImageView back;
@FXML
private Canvas canvas;
@FXML
private HBox bar;
public void initialize() {
setStatus("test");
}
@Override
public void setStage(Stage s) {
System.out.println("Setting stage");
canvas.widthProperty().bind(s.getScene().widthProperty());
canvas.heightProperty().bind(s.getScene().heightProperty().subtract(bar.heightProperty()));
back.setPreserveRatio(false);
back.fitWidthProperty().bind(canvas.widthProperty());
back.fitHeightProperty().bind(canvas.heightProperty());
Platform.runLater(() -> {
GraphicsContext context = canvas.getGraphicsContext2D();
context.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
{
int x = 20, y = 20;
context.setFill(Color.BLACK);
context.fillRect(x, y, x + 50, y + 50);
}
{
int x = 50, y = 20;
context.setFill(Color.BLUE);
context.fillOval(x, y, x + 50, y + 50);
}
});
}
public void setStatus(String text) {
status.setText(text);
}
}
package fi.utu.tech.demo;
public class Main {
public static void main(String[] args) {
MainApp.launch(MainApp.class, args);
}
}
package fi.utu.tech.demo;
import fi.utu.tech.distributed.classloader.hotreload.HotReloader;
import fi.utu.tech.hotreload.HotReloadMonitor;
import fi.utu.tech.hotreload.ReloadableApplication;
import fi.utu.tech.hotreload.SceneLoader;
import javafx.application.Application;
import javafx.stage.Stage;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.util.List;
public class MainApp extends Application implements ReloadableApplication {
public List<String> createStyles() {
return List.of(ResourceLoader.stylesheet("styles.css"));
}
public SceneLoader getSceneLoader() {
return new SceneLoaderImpl();
}
@Override
public boolean useGradle() {
return true;
}
@Override
public void start(final Stage stage) {
try {
HotReloadMonitor monitor = new HotReloadMonitor(
this, stage, "fi.utu.tech.demo",
"hotreload-demo/"+HotReloader.gradlePath.toString(),
List.of(
HotReloader.gradlePath.toString(),
HotReloader.ideaPath.toString(),
HotReloader.mavenPath.toString()
));
monitor.start();
} catch (Exception e) {
System.err.println(e.getMessage());
}
stage.setResizable(true);
stage.show();
stage.setWidth(800);
stage.setHeight(800);
setScene(stage, getSceneLoader());
}
}
package fi.utu.tech.demo;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
public class ResourceLoader<N extends Parent, C> {
public final N root;
public final C controller;
protected Class<?> resourceRoot() {
return getClass();
}
public ResourceLoader(String contentPath) {
N root_ = null;
C controller_ = null;
try {
// determines where to look for the resources (the root path)
Class<?> resourceRootClass = resourceRoot();
FXMLLoader loader = new FXMLLoader(resourceRootClass.getResource(contentPath));
root_ = loader.load();
controller_ = loader.getController();
//System.out.println("DEBUG: " + contentPath + " loaded.");
} catch (Exception e) {
e.printStackTrace();
//System.exit(1);
}
root = root_;
controller = controller_;
}
// finds stylesheets both outside and inside jars
public static String stylesheet(String fileName) {
return ResourceLoader.class.getResource(fileName).toExternalForm();
}
}
package fi.utu.tech.demo;
import fi.utu.tech.hotreload.Controller;
import fi.utu.tech.hotreload.SceneLoader;
import javafx.scene.Parent;
public class SceneLoaderImpl implements SceneLoader {
final ResourceLoader<Parent, BoardController> loader = new ResourceLoader<>("board.fxml");
public SceneLoaderImpl() {
}
@Override
public Parent root() {
return loader.root;
}
@Override
public Controller controller() {
return loader.controller;
}
}
module fi.utu.tech.demo {
requires transitive javafx.fxml;
requires fi.utu.tech.hotreload;
opens fi.utu.tech.demo;
requires java.logging;
exports fi.utu.tech.demo;
}
hotreload-demo/src/main/resources/fi/utu/tech/demo/alien.png

45 KiB

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/14.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fi.utu.tech.demo.BoardController">
<bottom>
<HBox fx:id="bar" alignment="CENTER_LEFT" prefHeight="32.0" BorderPane.alignment="CENTER">
<children>
<Label fx:id="status" text="Status" />
</children>
</HBox>
</bottom>
<center>
<StackPane prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<children>
<ImageView fx:id="back" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@alien.png" />
</image>
</ImageView>
<Canvas fx:id="canvas" height="200.0" width="200.0" />
</children>
</StackPane>
</center>
</BorderPane>
.active-button {
-fx-background-color: gray;
}
package fi.utu.tech.demo;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class DummyTest {
@Test
public void dummy() throws Exception {
assertTrue(true);
}
}
//
// Patch, i.e. configure, Java module system at test-runtime
//
// Allow deep reflection for test discovery - repeat for each test library you need
--add-opens
fi.utu.tech.demo/fi.utu.tech.demo=org.junit.platform.commons
// "requires org.junit.jupiter.api"
--add-reads
fi.utu.tech.demo=org.junit.jupiter.api
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>hotreload</artifactId>
<version>1.0.0</version>
<name>Hot reloader</name>
<url>https://gitlab.utu.fi/tech/education/gui/hotreload</url>
<packaging>jar</packaging>
<parent>
<groupId>fi.utu.tech</groupId>
<artifactId>hotreload-meta</artifactId>
<version>1.0.0</version>
</parent>
<build>
<plugins>
<!-- Run this app with exec:java -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<skip>true</skip>
<mainClass>none</mainClass>
</configuration>
</plugin>
<!-- Make the packaged jar executable -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<!-- DO NOT include log4j.properties file in your Jar -->
<excludes>
<exclude>**/log4j.properties</exclude>
</excludes>
<archive>
<manifest>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
package fi.utu.tech.hotreload;
import javafx.stage.Stage;
public interface Controller {
default void setStage(Stage s) {}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment