Created
July 29, 2015 01:13
-
-
Save Groostav/ff35eb2d19b348f2e25c to your computer and use it in GitHub Desktop.
FXMLLoader, FXController, ViewByNamingConventionFinder, PreloadedFX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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