Skip to content

Instantly share code, notes, and snippets.

@Groostav
Created July 29, 2015 01:13
Show Gist options
  • Save Groostav/ff35eb2d19b348f2e25c to your computer and use it in GitHub Desktop.
Save Groostav/ff35eb2d19b348f2e25c to your computer and use it in GitHub Desktop.
FXMLLoader, FXController, ViewByNamingConventionFinder, PreloadedFX
package com.empowerops.common.ui;
/**
* Intention contract that specifies that a component is a JavaFX controller.
*
* Created by Geoff on 2014-05-28.
*/
public interface FXController {
}
package com.empowerops.common.ui;
/**
* Defines a class that will load its FXML components <i>before</i> the constructor is called.
* This class exists purely for convenience and to make use of the in-line final field declaration.
*
* <p>Consider a controller that wishes to have a constant string that has the text equal to a string label text value.
* We would like to declare this string final, but cannot, because the FXML loader must first bind the controller to
* a view (including the above mentioned label) <i>before</i> we can get access to a label's text. IE, traditionally
* your code needs to look like this:
* <pre>{@code
* class ControllerClass{
* private final String textFieldValue;
* @literal@FXML Label textField;
*
* public ControllerClass(FXMLLoader loader, Dependency dependency){
* loader.load(this);
* this.textFieldValue = textField.getText();
* //...
* }
* }
* }</pre>
* which is annoying, since we'd really like to in-line that variable. With preloaded FX, because of the way super() works,
* we can do this:
* <pre>{@code
* class ControllerClass extends {@link PreloadedFX}{
* {@literal @}FXML Label textField;
* private final String textFieldValue = textField.getText();
*
* public ControllerClass(FXMLLoader loader, Dependency dependency){
* super(loader);
* //...
* }
* }
* }</pre>
*
* which is just a little bit nicer :) </p>
*
* Created by Geoff on 03/07/2014.
*/
public abstract class PreloadedFX implements FXController {
public PreloadedFX(FXMLLoader loader){
// because this call to loader is in super(), it will take precedence over all constructor/initializer
// stuff in derived classes.
loader.load(this);
}
}
package com.empowerops.common.ui;
import com.google.inject.Inject;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
/**
* Created by Geoff on 2014-06-06.
*/
public class ViewByNamingConventionFinder {
private final Class explicitControllerClass;
private Class<? extends FXController> controllerClass;
private String viewName;
private URL viewResource;
private boolean attemptedLocatingFile = false;
@Inject
public ViewByNamingConventionFinder() {
this.explicitControllerClass = null;
}
public void locateViewFileFor(FXController parentController) {
controllerClass = updateForController(parentController);
assertNameEndsWithController(parentController);
String name = getSuggestedViewName();
viewResource = parentController.getClass().getResource(name);
viewName = name;
attemptedLocatingFile = true;
}
public String getSuggestedViewFullyQualifiedPath(){
StringBuilder builder = new StringBuilder();
builder.append(controllerClass.getCanonicalName());
builder.replace(
builder.indexOf(controllerClass.getSimpleName()),
builder.length(),
getSuggestedViewName()
);
return builder.toString();
}
public String getSuggestedViewName() {
StringBuilder builder = new StringBuilder(controllerClass.getSimpleName());
builder.replace(builder.length() - "Controller".length(), builder.length(), "View");
builder.append(".fxml");
return builder.toString();
}
public boolean hasLocatedViewFile() {
try {
return attemptedLocatingFile && Paths.get(viewResource.toURI()).toFile().exists();
}
catch (URISyntaxException e) {
return false;
}
}
public URL getViewFile() {
assert attemptedLocatingFile;
return viewResource;
}
public String getViewName() {
assert attemptedLocatingFile;
return viewName;
}
public String getControllerName() {
assert attemptedLocatingFile;
return controllerClass.getSimpleName();
}
private Class<? extends FXController> updateForController(FXController parentController) {
return explicitControllerClass == null ? parentController.getClass() : explicitControllerClass;
}
private void assertNameEndsWithController(FXController parentController) {
if ( ! controllerClass.getSimpleName().endsWith("Controller")) {
throw new IllegalStateException(
"the controller '" + parentController.getClass().getSimpleName() + "' " +
"must follow our naming convention to allow its view file to be resolved automagically"
);
}
}
}
package com.empowerops.common.ui;
import com.google.inject.Inject;
import com.google.inject.Provider;
import javafx.application.Platform;
import javafx.fxml.FXML;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
/**
* Created by Geoff on 2014-05-26.
*/
public class FXMLLoader {
// private static final boolean DisplayResponsivenessSpinner = getEnvBool(FXMLLoader.class, "DisplayResponsivenessSpinner").orElse(false);
private static final Logger log = Logger.getLogger(FXMLLoader.class.getCanonicalName());
//some objects may keep their FXML loader, and we dont want to keep a reference to a provider of a stale
//so this is set null when we're done.
private Provider<ViewByNamingConventionFinder> finderProvider;
private FXController controller;
private boolean wasBound;
@Inject
public FXMLLoader(Provider<ViewByNamingConventionFinder> finderProvider) {
this.finderProvider = finderProvider;
}
public void load(FXController controller) {
assert this.getController() == null : "improper life cycle, only 1 load call allowed per object instance. " +
"You cannot reuse FXMLLoaders.";
if (! Platform.isFxApplicationThread()) {
log.info("loading FXML from non-UI thread: " + Thread.currentThread());
}
this.controller = controller;
this.wasBound = false;
ViewByNamingConventionFinder location = findViewByConvention();
javafx.fxml.FXMLLoader fxmlLoader = new javafx.fxml.FXMLLoader();
fxmlLoader.setControllerFactory(this::yieldController);
fxmlLoader.setLocation(location.getViewFile());
runImmediatelyOnFXThread(() -> instanceViewTree(fxmlLoader, location));
assertControllerWasBound();
assertAllFieldsBound(location);
finderProvider = null;
}
private static void runImmediatelyOnFXThread(Runnable runnable){
if (Platform.isFxApplicationThread()) {
runnable.run();
return;
}
AtomicReference<Throwable> exceptionRaisedByJavaFXAction = new AtomicReference<>(null);
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
runnable.run();
}
catch (Exception | AssertionError error) {
exceptionRaisedByJavaFXAction.set(error);
}
finally {
latch.countDown();
}
});
try {
latch.await();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (exceptionRaisedByJavaFXAction.get() != null) {
throw new RuntimeException(exceptionRaisedByJavaFXAction.get());
}
}
private void assertControllerWasBound() {
if ( ! wasBound){
throw new FXMLBindingException(
"The FXML file does not contain the attribute fx:controller=\"fully.qualified.controller\"" + "\n" +
"Please ensure the root FXML element contains the attribute:" + "\n" +
"fx:controller=" + getController().getClass().getCanonicalName()
);
}
}
private void assertAllFieldsBound(ViewByNamingConventionFinder location) {
for(Field uiField : getUIElements(getController())){
String reason =
"No element with an fx:id '" + uiField.getName() + "' exists on the FXML document at \n" +
location.getViewName() + "\n" +
"The @FXML field's name should match the fx:id attribute on the element exactly." + "\n" +
"(did you use the CSS attribute 'id' and not the JavaFX attribute 'fx:id'?";
assertFieldIsNotNull(getController(), uiField, reason);
}
}
private FXController yieldController(Class<?> viewDeclaredControllerType) {
if( ! viewDeclaredControllerType.isInstance(getController())){
throw new FXMLBindingException("controller types do not match:\n" +
"The controller being loaded is of type " + getController().getClass().getSimpleName() + "\n" +
"And the controller declared as the controller class in the fxml file is " + viewDeclaredControllerType.getSimpleName() + "."
);
}
this.wasBound = true;
return getController();
}
protected Iterable<Field> getUIElements(FXController controller){
List<Field> results = new ArrayList<>();
Class classToInspect = controller.getClass();
while(classToInspect != null){
for(Field field : classToInspect.getDeclaredFields()){
if( ! field.isAnnotationPresent(FXML.class)){
continue;
}
field.setAccessible(true);
results.add(field);
}
classToInspect = classToInspect.getSuperclass();
}
return results;
// return ReflectionUtilities.getAllFields(controller).where(field -> field.isAnnotationPresent(FXML.class));
}
private void instanceViewTree(javafx.fxml.FXMLLoader fxmlLoader,
ViewByNamingConventionFinder location) {
try {
fxmlLoader.load();
}
catch (IOException exception) {
if(exception.getCause() instanceof FXMLBindingException){
throw (FXMLBindingException) exception.getCause();
}
else if (exception.getCause() instanceof ClassNotFoundException){
String nonExistentType = exception.getCause().getMessage();
throw new FXMLBindingException(
"The specified fx:controller '" + nonExistentType + "'\n" +
"it should be '" + getController().getClass().getCanonicalName() + "'",
exception
);
}
else throw new FXMLBindingException(
"A generic FXML binding exception has occured. See below for the relavent FXML line number.\n" +
"Please check to make sure the FXML file is valid XML, that and that it declares the correct controller.",
exception);
}
}
private ViewByNamingConventionFinder findViewByConvention() {
ViewByNamingConventionFinder locator = finderProvider.get();
locator.locateViewFileFor(getController());
assertLocatorHasFoundView(locator);
return locator;
}
private void assertLocatorHasFoundView(ViewByNamingConventionFinder locator) {
if( ! locator.hasLocatedViewFile()){
throw new FXMLBindingException(
"The view for " + getController().getClass().getCanonicalName() + " could not be found.\n" +
"Please ensure that " + locator.getSuggestedViewFullyQualifiedPath() + " " +
"(or " + locator.getSuggestedViewName() + " at another suitable directory) " +
"exists."
);
}
}
public static void assertFieldIsNotNull(Object object, Field field, String reason){
boolean wasAccessible = field.isAccessible();
field.setAccessible(true);
try{
if(field.get(object) == null){
throw new FXMLBindingException(reason);
}
else{
return;
}
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
finally{
field.setAccessible(wasAccessible);
}
}
public FXController getController() {
return controller;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment