Add a Table that shows one Row per element, with sort and filter.
The collection keeps its own order. Header clicks and the popup sort one Column, and the checklist or a typed comparison decides which Rows stay visible. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.WildcardType;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Reads an Attribute's declared type from a Model.
|
||||
*/
|
||||
final class Attributes {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private Attributes() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element type of a collection Attribute.
|
||||
*
|
||||
* @param model the Model that holds the collection
|
||||
* @param name the collection Attribute
|
||||
* @return the element class
|
||||
*/
|
||||
static Class<?> elementType(Object model, String name) {
|
||||
Method reader = reader(model.getClass(), name);
|
||||
Class<?> element = classArgument(reader.getGenericReturnType());
|
||||
if (element == null) {
|
||||
throw new IllegalArgumentException("No element type for \"" + name + "\"");
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the declared type of one Attribute.
|
||||
*
|
||||
* @param type the Model class
|
||||
* @param name the Attribute
|
||||
* @return the declared type
|
||||
*/
|
||||
static Class<?> type(Class<?> type, String name) {
|
||||
return reader(type, name).getReturnType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a Column can use this declared type.
|
||||
* <p>
|
||||
* A string, number, boolean, enum, or date or time is simple. A type with its own
|
||||
* Attributes is not.
|
||||
*
|
||||
* @param type the declared type
|
||||
* @return {@code true} when the type is a simple value
|
||||
*/
|
||||
static boolean simple(Class<?> type) {
|
||||
if (type == String.class || type == Boolean.class || type == boolean.class || type.isEnum()) {
|
||||
return true;
|
||||
}
|
||||
if (type.isPrimitive()) {
|
||||
return type != char.class && type != void.class;
|
||||
}
|
||||
if (Number.class.isAssignableFrom(type)) {
|
||||
return true;
|
||||
}
|
||||
if (Date.class.isAssignableFrom(type) || Calendar.class.isAssignableFrom(type)) {
|
||||
return true;
|
||||
}
|
||||
return TemporalAccessor.class.isAssignableFrom(type) || TemporalAmount.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the read method of a read-write Attribute.
|
||||
*
|
||||
* @param type the Model class
|
||||
* @param name the Attribute
|
||||
* @return the getter
|
||||
*/
|
||||
private static Method reader(Class<?> type, String name) {
|
||||
for (PropertyDescriptor descriptor : descriptors(type)) {
|
||||
if (descriptor.getName().equals(name)
|
||||
&& descriptor.getReadMethod() != null
|
||||
&& descriptor.getWriteMethod() != null) {
|
||||
return descriptor.getReadMethod();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No attribute \"" + name + "\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bean attributes declared on {@code type}.
|
||||
*
|
||||
* @param type the Model class
|
||||
* @return the attributes
|
||||
*/
|
||||
private static List<PropertyDescriptor> descriptors(Class<?> type) {
|
||||
try {
|
||||
BeanInfo info = Introspector.getBeanInfo(type, Object.class);
|
||||
return List.of(info.getPropertyDescriptors());
|
||||
} catch (IntrospectionException failure) {
|
||||
throw new IllegalStateException("Could not introspect " + type.getName(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single type argument of a collection type.
|
||||
*
|
||||
* @param type the generic return type
|
||||
* @return the element class, or {@code null} when it cannot be read
|
||||
*/
|
||||
private static Class<?> classArgument(Type type) {
|
||||
if (!(type instanceof ParameterizedType parameterized)
|
||||
|| parameterized.getActualTypeArguments().length != 1) {
|
||||
return null;
|
||||
}
|
||||
Type argument = parameterized.getActualTypeArguments()[0];
|
||||
if (argument instanceof Class<?> raw) {
|
||||
return raw;
|
||||
}
|
||||
if (argument instanceof WildcardType wildcard && wildcard.getUpperBounds().length == 1) {
|
||||
Type bound = wildcard.getUpperBounds()[0];
|
||||
if (bound instanceof Class<?> raw) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
/**
|
||||
* One Column of a {@link Table}.
|
||||
* <p>
|
||||
* The Column names the header the developer sees and the Attribute of the row element
|
||||
* that the header, the Filter, and the Sort share.
|
||||
*/
|
||||
public class Column {
|
||||
|
||||
/**
|
||||
* The header text.
|
||||
*/
|
||||
private String header;
|
||||
|
||||
/**
|
||||
* The Attribute of the row element.
|
||||
*/
|
||||
private String attribute;
|
||||
|
||||
/**
|
||||
* Sets the header text.
|
||||
*
|
||||
* @param header the header text
|
||||
*/
|
||||
public void setHeader(String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header text.
|
||||
*
|
||||
* @return the header text
|
||||
*/
|
||||
public String getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Attribute name.
|
||||
*
|
||||
* @param attribute the Attribute of the row element
|
||||
*/
|
||||
public void setAttribute(String attribute) {
|
||||
this.attribute = attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Attribute name.
|
||||
*
|
||||
* @return the Attribute of the row element
|
||||
*/
|
||||
public String getAttribute() {
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,9 @@ import java.net.URL;
|
||||
*/
|
||||
public final class ViewLoader {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private ViewLoader() {
|
||||
}
|
||||
|
||||
@@ -93,6 +96,7 @@ public final class ViewLoader {
|
||||
for (BindingList bindingList : session.bindingLists()) {
|
||||
bindingList.apply(loader.getNamespace(), viewModel, controller, root, context);
|
||||
}
|
||||
Table.openAll(root, viewModel, context);
|
||||
return new LoadedView(root, viewModel);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Could not load view " + fxml, failure);
|
||||
@@ -101,6 +105,13 @@ public final class ViewLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the container for the Controller class named by the document.
|
||||
*
|
||||
* @param context the container
|
||||
* @param type the Controller class
|
||||
* @return the Controller
|
||||
*/
|
||||
private static Object controller(ApplicationContext context, Class<?> type) {
|
||||
return context.getComponent(type.asSubclass(Object.class));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.component.VisitRow;
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* A Column checklist shows only the confirmed values.
|
||||
*/
|
||||
class FilterByChecklistTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirming a subset hides the rest, and the same Row comes back when the Filter is cleared.
|
||||
*/
|
||||
@Test
|
||||
void confirmingASubsetHidesTheOtherRows() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Visit oak = new Visit("Oak", "Bonn");
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(main, oak)));
|
||||
host.setSelectedVisit(oak);
|
||||
LoadedView loaded = load(host);
|
||||
Table table = table(loaded);
|
||||
VisitRow oakRow = (VisitRow) body(table).getChildren().get(1);
|
||||
openCity(table);
|
||||
uncheck(table, "Bonn");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Main"), streets(table));
|
||||
assertTrue(opener(table, 1).getStyleClass().contains("filtered"));
|
||||
assertEquals(List.of("Main", "Oak"), streets(host.getVisits()));
|
||||
assertSame(oak, host.getSelectedVisit());
|
||||
assertFalse(body(table).getChildren().stream()
|
||||
.anyMatch(node -> node.getStyleClass().contains(Table.SELECTED_STYLE)));
|
||||
openCity(table);
|
||||
command(table, "Clear filter").fire();
|
||||
assertSame(oakRow, body(table).getChildren().get(1));
|
||||
assertEquals(List.of("Main", "Oak"), streets(table));
|
||||
assertFalse(opener(table, 1).getStyleClass().contains("filtered"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* All checked is no Filter. A later value is shown. None checked hides every Row.
|
||||
*/
|
||||
@Test
|
||||
void allCheckedLeavesNoFilterAndNoneCheckedHidesEveryRow() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(main, new Visit("Oak", null))));
|
||||
LoadedView loaded = load(host);
|
||||
Table table = table(loaded);
|
||||
openCity(table);
|
||||
List<String> labels = checks(table).stream().map(CheckBox::getText).toList();
|
||||
assertEquals(List.of("", "Berlin"), labels);
|
||||
command(table, "Confirm").fire();
|
||||
assertFalse(opener(table, 1).getStyleClass().contains("filtered"));
|
||||
@SuppressWarnings("unchecked")
|
||||
ObservableList<Visit> visits = (ObservableList<Visit>) loaded.property("visits").getValue();
|
||||
visits.add(new Visit("Elm", "Hamburg"));
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(table));
|
||||
openCity(table);
|
||||
for (CheckBox box : checks(table)) {
|
||||
box.setSelected(false);
|
||||
}
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of(), streets(table));
|
||||
assertTrue(opener(table, 1).getStyleClass().contains("filtered"));
|
||||
visits.add(new Visit("Pine", "Kiel"));
|
||||
assertEquals(List.of(), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An edit that fails the Filter hides that Row and keeps the new value on the entity.
|
||||
*/
|
||||
@Test
|
||||
void anEditThatFailsTheFilterHidesTheRow() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(main, new Visit("Oak", "Bonn"))));
|
||||
Table table = table(load(host));
|
||||
openCity(table);
|
||||
uncheck(table, "Bonn");
|
||||
command(table, "Confirm").fire();
|
||||
TextField city = (TextField) body(table).getChildren().get(0).lookup("#cityField");
|
||||
city.setText("Bonn");
|
||||
assertEquals(List.of(), streets(table));
|
||||
assertEquals("Bonn", main.getCity());
|
||||
openCity(table);
|
||||
assertTrue(checks(table).stream().anyMatch(box -> "Bonn".equals(box.getText())));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A click outside the popup keeps the previous Filter.
|
||||
*/
|
||||
@Test
|
||||
void aClickOutsideKeepsThePreviousFilter() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(new Visit("Main", "Berlin"), new Visit("Oak", "Bonn"))));
|
||||
Table table = table(load(host));
|
||||
openCity(table);
|
||||
uncheck(table, "Bonn");
|
||||
table.fireEvent(click());
|
||||
assertEquals(List.of("Main", "Oak"), streets(table));
|
||||
assertFalse(popup(table).isVisible());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the selection view, which names the same Columns.
|
||||
*
|
||||
* @param host the model
|
||||
* @return the loaded view
|
||||
*/
|
||||
private static LoadedView load(Host host) {
|
||||
URL fxml = FilterByChecklistTest.class.getResource("show-visits-selection.fxml");
|
||||
assertNotNull(fxml);
|
||||
return ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table.
|
||||
*
|
||||
* @param loaded the loaded view
|
||||
* @return the table
|
||||
*/
|
||||
private static Table table(LoadedView loaded) {
|
||||
return (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the body
|
||||
*/
|
||||
private static VBox body(Table table) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
return (VBox) scroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the opener of one Column.
|
||||
*
|
||||
* @param table the table
|
||||
* @param index the Column index
|
||||
* @return the opener
|
||||
*/
|
||||
private static javafx.scene.control.Button opener(Table table, int index) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
HBox header = (HBox) headerScroll.getContent();
|
||||
return (javafx.scene.control.Button) header.getChildren().get(index).lookup("." + Table.OPENER_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the City popup.
|
||||
*
|
||||
* @param table the table
|
||||
*/
|
||||
private static void openCity(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
HBox header = (HBox) headerScroll.getContent();
|
||||
((javafx.scene.control.Button) header.getChildren().get(1).lookup("." + Table.OPENER_STYLE)).fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the popup.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the popup
|
||||
*/
|
||||
private static VBox popup(Table table) {
|
||||
return (VBox) table.lookup("." + Table.POPUP_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checklist boxes.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the boxes
|
||||
*/
|
||||
private static List<CheckBox> checks(Table table) {
|
||||
List<CheckBox> boxes = new ArrayList<>();
|
||||
for (Node node : popup(table).getChildren()) {
|
||||
if (node instanceof VBox list) {
|
||||
for (Node child : list.getChildren()) {
|
||||
if (child instanceof CheckBox box) {
|
||||
boxes.add(box);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unchecks the box with the given text.
|
||||
*
|
||||
* @param table the table
|
||||
* @param text the checklist text
|
||||
*/
|
||||
private static void uncheck(Table table, String text) {
|
||||
for (CheckBox box : checks(table)) {
|
||||
if (text.equals(box.getText())) {
|
||||
box.setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a popup command.
|
||||
*
|
||||
* @param table the table
|
||||
* @param text the command text
|
||||
* @return the button
|
||||
*/
|
||||
private static Button command(Table table, String text) {
|
||||
return popup(table).getChildren().stream()
|
||||
.filter(Button.class::isInstance)
|
||||
.map(Button.class::cast)
|
||||
.filter(button -> text.equals(button.getText()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the visible rows.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(Table table) {
|
||||
List<String> streets = new ArrayList<>();
|
||||
for (Node node : body(table).getChildren()) {
|
||||
streets.add(((TextField) node.lookup("#streetField")).getText());
|
||||
}
|
||||
return streets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the collection.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(List<Visit> visits) {
|
||||
return visits.stream().map(Visit::getStreet).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a primary click.
|
||||
*
|
||||
* @return the event
|
||||
*/
|
||||
private static MouseEvent click() {
|
||||
return new MouseEvent(MouseEvent.MOUSE_CLICKED, 1, 1, 1, 1, MouseButton.PRIMARY, 1,
|
||||
false, false, false, false, true, false, false, false, false, false, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Role;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* A typed comparison replaces the checklist on that Column.
|
||||
*/
|
||||
class FilterByComparisonTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* A string comparison ignores case, and confirming the checklist replaces it.
|
||||
*/
|
||||
@Test
|
||||
void aStringComparisonReplacesTheChecklist() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit berlin = new Visit("Main", "Berlin");
|
||||
Visit bonn = new Visit("Oak", "Bonn");
|
||||
Visit lower = new Visit("Elm", "berlin");
|
||||
Host host = host(berlin, bonn, lower);
|
||||
Table table = first(load(host));
|
||||
open(table, 1);
|
||||
operator(table).setValue("Contains");
|
||||
value(table).setText("ER");
|
||||
command(table, "Confirm").fire();
|
||||
assertFalse(popup(table).isVisible());
|
||||
assertEquals(List.of("Main", "Elm"), streets(table));
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(host.getVisits()));
|
||||
open(table, 1);
|
||||
assertEquals("Contains", operator(table).getValue());
|
||||
assertEquals("ER", value(table).getText());
|
||||
operator(table).setValue(null);
|
||||
value(table).setText("");
|
||||
uncheck(table, "Berlin");
|
||||
uncheck(table, "berlin");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Oak"), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts with and ends with match the checklist text, ignoring case.
|
||||
*/
|
||||
@Test
|
||||
void startsWithAndEndsWithMatchTheChecklistText() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"), new Visit("Oak", "Bonn"));
|
||||
Table table = first(load(host));
|
||||
open(table, 1);
|
||||
operator(table).setValue("Starts with");
|
||||
value(table).setText("bo");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Oak"), streets(table));
|
||||
open(table, 1);
|
||||
operator(table).setValue("Ends with");
|
||||
value(table).setText("LIN");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Main"), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A number comparison excludes the boundary, and a missing value does not pass.
|
||||
*/
|
||||
@Test
|
||||
void aNumberComparisonExcludesTheBoundary() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit five = visit("Five", 5, false, Role.MEMBER, LocalDate.of(2020, 1, 2));
|
||||
Visit six = visit("Six", 6, true, Role.ADMIN, LocalDate.of(2020, 1, 1));
|
||||
Visit missing = visit("Missing", null, false, null, null);
|
||||
Host host = host(five, six, missing);
|
||||
Table table = first(load(host));
|
||||
open(table, 2);
|
||||
operator(table).setValue("Greater than");
|
||||
value(table).setText("5");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Six"), streets(table));
|
||||
open(table, 2);
|
||||
value(table).setText("abc");
|
||||
command(table, "Confirm").fire();
|
||||
assertTrue(popup(table).isVisible());
|
||||
assertEquals(List.of("Six"), streets(table));
|
||||
value(table).setText("");
|
||||
command(table, "Confirm").fire();
|
||||
assertTrue(popup(table).isVisible());
|
||||
operator(table).setValue(null);
|
||||
value(table).setText("5");
|
||||
command(table, "Confirm").fire();
|
||||
assertTrue(popup(table).isVisible());
|
||||
assertEquals(List.of("Six"), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Before and after exclude the named day.
|
||||
*/
|
||||
@Test
|
||||
void beforeAndAfterExcludeTheNamedDay() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit earlier = visit("Early", 1, false, Role.MEMBER, LocalDate.of(2020, 1, 1));
|
||||
Visit day = visit("Day", 1, false, Role.MEMBER, LocalDate.of(2020, 1, 2));
|
||||
Host host = host(earlier, day);
|
||||
Table table = first(load(host));
|
||||
open(table, 5);
|
||||
operator(table).setValue("Before");
|
||||
value(table).setText("2020-01-02");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Early"), streets(table));
|
||||
open(table, 5);
|
||||
operator(table).setValue("After");
|
||||
value(table).setText("2020-01-01");
|
||||
command(table, "Confirm").fire();
|
||||
assertEquals(List.of("Day"), streets(table));
|
||||
open(table, 5);
|
||||
value(table).setText("02.01.2020");
|
||||
command(table, "Confirm").fire();
|
||||
assertTrue(popup(table).isVisible());
|
||||
assertEquals(List.of("Day"), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A boolean and an enum have no comparison.
|
||||
*/
|
||||
@Test
|
||||
void aBooleanAndAnEnumHaveNoComparison() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = host(visit("Main", 1, true, Role.MEMBER, LocalDate.of(2020, 1, 1)));
|
||||
Table table = first(load(host));
|
||||
open(table, 3);
|
||||
assertFalse(operator(table).isVisible());
|
||||
assertFalse(value(table).isVisible());
|
||||
open(table, 4);
|
||||
assertFalse(operator(table).isVisible());
|
||||
assertFalse(value(table).isVisible());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a visit with the extra Attributes filled in.
|
||||
*
|
||||
* @param street the street
|
||||
* @param rank the rank
|
||||
* @param open whether the visit is open
|
||||
* @param role the role
|
||||
* @param day the day
|
||||
* @return the visit
|
||||
*/
|
||||
private static Visit visit(String street, Integer rank, boolean open, Role role, LocalDate day) {
|
||||
Visit visit = new Visit(street, "Berlin");
|
||||
visit.setRank(rank);
|
||||
visit.setOpen(open);
|
||||
visit.setRole(role);
|
||||
visit.setDay(day);
|
||||
return visit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a host with those visits.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the host
|
||||
*/
|
||||
private static Host host(Visit... visits) {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(visits)));
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the two-table view.
|
||||
*
|
||||
* @param host the model
|
||||
* @return the loaded view
|
||||
*/
|
||||
private static LoadedView load(Host host) {
|
||||
URL fxml = FilterByComparisonTest.class.getResource("show-two-tables.fxml");
|
||||
assertNotNull(fxml);
|
||||
return ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first table.
|
||||
*
|
||||
* @param loaded the loaded view
|
||||
* @return the table
|
||||
*/
|
||||
private static Table first(LoadedView loaded) {
|
||||
return (Table) loaded.getRoot().lookup("#firstTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens one Column's popup.
|
||||
*
|
||||
* @param table the table
|
||||
* @param index the Column index
|
||||
*/
|
||||
private static void open(Table table, int index) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
HBox header = (HBox) headerScroll.getContent();
|
||||
((Button) header.getChildren().get(index).lookup("." + Table.OPENER_STYLE)).fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the popup.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the popup
|
||||
*/
|
||||
private static VBox popup(Table table) {
|
||||
return (VBox) table.lookup("." + Table.POPUP_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the comparison operator.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the operator
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ComboBox<String> operator(Table table) {
|
||||
return (ComboBox<String>) popup(table).lookup(".column-comparison-operator");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the comparison value.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the value
|
||||
*/
|
||||
private static TextField value(Table table) {
|
||||
return (TextField) popup(table).lookup(".column-comparison-value");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unchecks the checklist entry with the given text.
|
||||
*
|
||||
* @param table the table
|
||||
* @param text the checklist text
|
||||
*/
|
||||
private static void uncheck(Table table, String text) {
|
||||
for (Node node : popup(table).getChildren()) {
|
||||
if (!(node instanceof VBox list)) {
|
||||
continue;
|
||||
}
|
||||
for (Node child : list.getChildren()) {
|
||||
if (child instanceof CheckBox box && text.equals(box.getText())) {
|
||||
box.setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a popup command.
|
||||
*
|
||||
* @param table the table
|
||||
* @param text the command text
|
||||
* @return the button
|
||||
*/
|
||||
private static Button command(Table table, String text) {
|
||||
return popup(table).getChildren().stream()
|
||||
.filter(Button.class::isInstance)
|
||||
.map(Button.class::cast)
|
||||
.filter(button -> text.equals(button.getText()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the visible rows.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(Table table) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
VBox body = (VBox) scroll.getContent();
|
||||
List<String> streets = new ArrayList<>();
|
||||
for (Node node : body.getChildren()) {
|
||||
streets.add(((TextField) node.lookup("#streetField")).getText());
|
||||
}
|
||||
return streets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the collection.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(List<Visit> visits) {
|
||||
return visits.stream().map(Visit::getStreet).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Runs a check on the JavaFX application thread.
|
||||
*/
|
||||
final class FxThread {
|
||||
|
||||
/**
|
||||
* Prevents instantiation.
|
||||
*/
|
||||
private FxThread() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once.
|
||||
*
|
||||
* @throws InterruptedException when the toolkit does not start
|
||||
*/
|
||||
static void start() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
try {
|
||||
Platform.startup(started::countDown);
|
||||
} catch (IllegalStateException alreadyStarted) {
|
||||
started.countDown();
|
||||
}
|
||||
if (!started.await(10, TimeUnit.SECONDS)) {
|
||||
fail("JavaFX toolkit did not start");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
return;
|
||||
}
|
||||
AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
action.run();
|
||||
} catch (Throwable failure) {
|
||||
thrown.set(failure);
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
if (!done.await(10, TimeUnit.SECONDS)) {
|
||||
fail("JavaFX thread did not finish");
|
||||
}
|
||||
Throwable failure = thrown.get();
|
||||
if (failure instanceof Exception exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* Dragging a Column boundary changes that Column's width only.
|
||||
*/
|
||||
class ResizeAColumnTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* A drag widens one Column and leaves the other, the Sort, and the order alone.
|
||||
*/
|
||||
@Test
|
||||
void aDragChangesOnlyThatColumn() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(new Visit("Main", "Berlin"))));
|
||||
Table table = table(host);
|
||||
HBox header = header(table);
|
||||
Region street = (Region) header.getChildren().get(0);
|
||||
Region city = (Region) header.getChildren().get(1);
|
||||
assertEquals(150, street.getPrefWidth());
|
||||
assertEquals(150, city.getPrefWidth());
|
||||
drag(street.lookup("." + Table.RESIZE_STYLE), 100, 140);
|
||||
assertEquals(190, street.getPrefWidth());
|
||||
assertEquals(150, city.getPrefWidth());
|
||||
double widened = street.getPrefWidth();
|
||||
street.lookup("." + Table.RESIZE_STYLE).fireEvent(click(2));
|
||||
assertEquals(widened, street.getPrefWidth());
|
||||
table.setPrefWidth(800);
|
||||
assertEquals(widened, street.getPrefWidth());
|
||||
assertEquals(150, city.getPrefWidth());
|
||||
assertEquals("Street", ((Label) street.lookup("." + Table.HEADER_TEXT_STYLE)).getText());
|
||||
assertEquals("City", ((Label) city.lookup("." + Table.HEADER_TEXT_STYLE)).getText());
|
||||
Label mark = (Label) street.lookup("." + Table.SORT_MARK_STYLE);
|
||||
assertFalse(mark.isVisible());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Columns wider than the Table make the header wider than the viewport.
|
||||
*/
|
||||
@Test
|
||||
void wideColumnsScrollHorizontally() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(new Visit("Main", "Berlin"))));
|
||||
Table table = table(host);
|
||||
HBox header = header(table);
|
||||
drag(((Region) header.getChildren().get(0)).lookup("." + Table.RESIZE_STYLE), 0, 400);
|
||||
assertEquals(700, header.getPrefWidth());
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
assertEquals(javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED, scroll.getHbarPolicy());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the visits table.
|
||||
*
|
||||
* @param host the model
|
||||
* @return the table
|
||||
*/
|
||||
private static Table table(Host host) {
|
||||
URL fxml = ResizeAColumnTest.class.getResource("show-visits.fxml");
|
||||
assertNotNull(fxml);
|
||||
LoadedView loaded = ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
return (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header row.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the header
|
||||
*/
|
||||
private static HBox header(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
return (HBox) headerScroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags a resize handle from one screen x to another.
|
||||
*
|
||||
* @param handle the handle
|
||||
* @param from the start screen x
|
||||
* @param to the end screen x
|
||||
*/
|
||||
private static void drag(javafx.scene.Node handle, double from, double to) {
|
||||
handle.fireEvent(mouse(MouseEvent.MOUSE_PRESSED, from, 1));
|
||||
handle.fireEvent(mouse(MouseEvent.MOUSE_DRAGGED, to, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a primary click.
|
||||
*
|
||||
* @param clicks the click count
|
||||
* @return the event
|
||||
*/
|
||||
private static MouseEvent click(int clicks) {
|
||||
return mouse(MouseEvent.MOUSE_CLICKED, 0, clicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a primary mouse event.
|
||||
*
|
||||
* @param type the event type
|
||||
* @param screen the screen x
|
||||
* @param clicks the click count
|
||||
* @return the event
|
||||
*/
|
||||
private static MouseEvent mouse(javafx.event.EventType<MouseEvent> type, double screen, int clicks) {
|
||||
return new MouseEvent(type, screen, 0, screen, 0, MouseButton.PRIMARY, clicks,
|
||||
false, false, false, false, true, false, false, false, false, false, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* The checklist search narrows entries and Select All still reaches the hidden ones.
|
||||
*/
|
||||
class SearchTheChecklistTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Typing narrows the list and leaves the checks and the Rows alone.
|
||||
*/
|
||||
@Test
|
||||
void typingNarrowsTheChecklistWithoutChangingTheRows() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"), new Visit("Oak", "Bonn"), new Visit("Elm", "Bern"));
|
||||
Table table = table(load(host));
|
||||
openCity(table);
|
||||
search(table).setText("ber");
|
||||
assertTrue(box(table, "Berlin").isVisible());
|
||||
assertTrue(box(table, "Bern").isVisible());
|
||||
assertFalse(box(table, "Bonn").isVisible());
|
||||
assertTrue(box(table, "Bonn").isSelected());
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(table));
|
||||
search(table).setText("*");
|
||||
assertFalse(box(table, "Berlin").isVisible());
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Each opening starts with an empty search, so every entry is visible.
|
||||
*/
|
||||
@Test
|
||||
void eachOpeningStartsWithAnEmptySearch() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"), new Visit("Oak", "Bonn"));
|
||||
Table table = table(load(host));
|
||||
openCity(table);
|
||||
search(table).setText("ber");
|
||||
command(table, "Confirm").fire();
|
||||
openCity(table);
|
||||
assertEquals("", search(table).getText());
|
||||
assertTrue(box(table, "Berlin").isVisible());
|
||||
assertTrue(box(table, "Bonn").isVisible());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select All reaches values the search box is hiding, and shows a mixed mark from the whole Column.
|
||||
*/
|
||||
@Test
|
||||
void selectAllReachesValuesTheSearchIsHiding() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"), new Visit("Oak", "Bonn"), new Visit("Elm", "Bern"));
|
||||
Table table = table(load(host));
|
||||
openCity(table);
|
||||
box(table, "Bonn").setSelected(false);
|
||||
search(table).setText("ber");
|
||||
assertFalse(box(table, "Bonn").isVisible());
|
||||
assertTrue(selectAll(table).isIndeterminate());
|
||||
assertFalse(selectAll(table).isSelected());
|
||||
click(selectAll(table));
|
||||
assertTrue(box(table, "Bonn").isSelected());
|
||||
search(table).setText("");
|
||||
assertTrue(box(table, "Bonn").isSelected());
|
||||
search(table).setText("ber");
|
||||
click(selectAll(table));
|
||||
assertFalse(box(table, "Bonn").isSelected());
|
||||
assertFalse(box(table, "Berlin").isSelected());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the visits table.
|
||||
*
|
||||
* @param host the model
|
||||
* @return the loaded view
|
||||
*/
|
||||
private static LoadedView load(Host host) {
|
||||
URL fxml = SearchTheChecklistTest.class.getResource("show-visits.fxml");
|
||||
assertNotNull(fxml);
|
||||
return ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a host with those visits.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the host
|
||||
*/
|
||||
private static Host host(Visit... visits) {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(visits)));
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table.
|
||||
*
|
||||
* @param loaded the loaded view
|
||||
* @return the table
|
||||
*/
|
||||
private static Table table(LoadedView loaded) {
|
||||
return (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the City popup.
|
||||
*
|
||||
* @param table the table
|
||||
*/
|
||||
private static void openCity(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
HBox header = (HBox) headerScroll.getContent();
|
||||
((Button) header.getChildren().get(1).lookup("." + Table.OPENER_STYLE)).fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the popup.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the popup
|
||||
*/
|
||||
private static VBox popup(Table table) {
|
||||
return (VBox) table.lookup("." + Table.POPUP_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search box.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the search box
|
||||
*/
|
||||
private static TextField search(Table table) {
|
||||
return (TextField) popup(table).lookup(".column-search");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Select All.
|
||||
*
|
||||
* @param table the table
|
||||
* @return Select All
|
||||
*/
|
||||
private static CheckBox selectAll(Table table) {
|
||||
return (CheckBox) popup(table).lookup(".column-select-all");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checklist box with the given text.
|
||||
*
|
||||
* @param table the table
|
||||
* @param text the checklist text
|
||||
* @return the box
|
||||
*/
|
||||
private static CheckBox box(Table table, String text) {
|
||||
for (Node node : popup(table).getChildren()) {
|
||||
if (!(node instanceof VBox list)) {
|
||||
continue;
|
||||
}
|
||||
for (Node child : list.getChildren()) {
|
||||
if (child instanceof CheckBox check && text.equals(check.getText())) {
|
||||
return check;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new AssertionError("No checklist entry " + text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a popup command.
|
||||
*
|
||||
* @param table the table
|
||||
* @param text the command text
|
||||
* @return the button
|
||||
*/
|
||||
private static Button command(Table table, String text) {
|
||||
return popup(table).getChildren().stream()
|
||||
.filter(Button.class::isInstance)
|
||||
.map(Button.class::cast)
|
||||
.filter(button -> text.equals(button.getText()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the visible rows.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(Table table) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
VBox body = (VBox) scroll.getContent();
|
||||
List<String> streets = new ArrayList<>();
|
||||
for (Node node : body.getChildren()) {
|
||||
streets.add(((TextField) node.lookup("#streetField")).getText());
|
||||
}
|
||||
return streets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a node with the primary button.
|
||||
*
|
||||
* @param node the node
|
||||
*/
|
||||
private static void click(Node node) {
|
||||
node.fireEvent(new MouseEvent(MouseEvent.MOUSE_CLICKED, 1, 1, 1, 1, MouseButton.PRIMARY, 1,
|
||||
false, false, false, false, true, false, false, false, false, false, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.component.VisitRow;
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Clicking a Row sets the parent Model's Selection.
|
||||
*/
|
||||
class SelectARowTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* A click selects the visit, including a click in a field, and a second click leaves it.
|
||||
*/
|
||||
@Test
|
||||
void aClickSelectsTheVisit() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Visit oak = new Visit("Oak", "Bonn");
|
||||
Host host = host(oak, main, oak);
|
||||
LoadedView loaded = load(host);
|
||||
VBox body = body(loaded);
|
||||
VisitRow mainRow = (VisitRow) body.getChildren().get(0);
|
||||
VisitRow oakRow = (VisitRow) body.getChildren().get(1);
|
||||
assertTrue(oakRow.getStyleClass().contains(Table.SELECTED_STYLE));
|
||||
assertFalse(mainRow.getStyleClass().contains(Table.SELECTED_STYLE));
|
||||
click(mainRow.lookup("#streetField"));
|
||||
assertSame(main, host.getSelectedVisit());
|
||||
assertTrue(mainRow.getStyleClass().contains(Table.SELECTED_STYLE));
|
||||
assertFalse(oakRow.getStyleClass().contains(Table.SELECTED_STYLE));
|
||||
click(mainRow);
|
||||
assertSame(main, host.getSelectedVisit());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A click on the blank body clears Selection. Removing the element clears it too.
|
||||
*/
|
||||
@Test
|
||||
void theBlankBodyAndRemovalClearSelection() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Visit oak = new Visit("Oak", "Bonn");
|
||||
Host host = host(main, main, oak);
|
||||
LoadedView loaded = load(host);
|
||||
VBox rows = body(loaded);
|
||||
click(rows);
|
||||
assertNull(host.getSelectedVisit());
|
||||
assertFalse(((VisitRow) rows.getChildren().get(0)).getStyleClass().contains(Table.SELECTED_STYLE));
|
||||
click(rows.getChildren().get(1));
|
||||
assertSame(oak, host.getSelectedVisit());
|
||||
@SuppressWarnings("unchecked")
|
||||
javafx.collections.ObservableList<Visit> visits =
|
||||
(javafx.collections.ObservableList<Visit>) loaded.property("visits").getValue();
|
||||
visits.remove(oak);
|
||||
assertNull(host.getSelectedVisit());
|
||||
assertEquals(1, rows.getChildren().size());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a host with the selection already set.
|
||||
*
|
||||
* @param selected the selected visit
|
||||
* @param visits the visits
|
||||
* @return the host
|
||||
*/
|
||||
private static Host host(Visit selected, Visit... visits) {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(visits)));
|
||||
host.setSelectedVisit(selected);
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the selection view.
|
||||
*
|
||||
* @param host the model
|
||||
* @return the loaded view
|
||||
*/
|
||||
private static LoadedView load(Host host) {
|
||||
URL fxml = SelectARowTest.class.getResource("show-visits-selection.fxml");
|
||||
assertNotNull(fxml);
|
||||
return ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the row body.
|
||||
*
|
||||
* @param loaded the loaded view
|
||||
* @return the body
|
||||
*/
|
||||
private static VBox body(LoadedView loaded) {
|
||||
Table table = (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
return (VBox) scroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a node with the primary button.
|
||||
*
|
||||
* @param node the node
|
||||
*/
|
||||
private static void click(javafx.scene.Node node) {
|
||||
node.fireEvent(new MouseEvent(MouseEvent.MOUSE_CLICKED, 1, 1, 1, 1, MouseButton.PRIMARY, 1,
|
||||
false, false, false, false, true, false, false, false, false, false, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.component.CityFirstRow;
|
||||
import de.neitzel.fx.view.component.VisitRow;
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* A Table shows one Row for each element, in collection order.
|
||||
*/
|
||||
class ShowATableOfRowsTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
try {
|
||||
Platform.startup(started::countDown);
|
||||
} catch (IllegalStateException alreadyStarted) {
|
||||
started.countDown();
|
||||
}
|
||||
if (!started.await(10, TimeUnit.SECONDS)) {
|
||||
fail("JavaFX toolkit did not start");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Each visit appears in collection order, with its fields under the headers.
|
||||
*/
|
||||
@Test
|
||||
void eachVisitAppearsInCollectionOrderUnderTheHeaders() throws Exception {
|
||||
onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Visit oak = new Visit("Oak", "Bonn");
|
||||
Host host = host(main, oak);
|
||||
LoadedView loaded = load("show-visits.fxml", host);
|
||||
Table table = table(loaded);
|
||||
HBox header = header(table);
|
||||
assertEquals("Street", headerText(header, 0));
|
||||
assertEquals("City", headerText(header, 1));
|
||||
VBox body = body(table);
|
||||
assertEquals(2, body.getChildren().size());
|
||||
VisitRow first = (VisitRow) body.getChildren().get(0);
|
||||
VisitRow second = (VisitRow) body.getChildren().get(1);
|
||||
assertSame(main, first.getModel());
|
||||
assertSame(oak, second.getModel());
|
||||
assertEquals("Main", field(first, "streetField").getText());
|
||||
assertEquals("Berlin", field(first, "cityField").getText());
|
||||
assertEquals("Oak", field(second, "streetField").getText());
|
||||
assertEquals("Bonn", field(second, "cityField").getText());
|
||||
assertEquals(150, field(first, "streetField").getPrefWidth());
|
||||
assertEquals(150, field(first, "cityField").getPrefWidth());
|
||||
assertEquals(150, ((javafx.scene.layout.Region) header.getChildren().get(0)).getPrefWidth());
|
||||
assertEquals(150, ((javafx.scene.layout.Region) header.getChildren().get(1)).getPrefWidth());
|
||||
first.setModel(new Visit("Other", "Hamburg"));
|
||||
assertEquals("Main", field(first, "streetField").getText());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A row keeps the height of its own fields.
|
||||
*/
|
||||
@Test
|
||||
void aRowKeepsTheHeightOfItsOwnFields() throws Exception {
|
||||
onFx(() -> {
|
||||
Host host = host(new Visit("Tall", "Berlin"), new Visit("Main", "Bonn"));
|
||||
LoadedView loaded = load("show-visits.fxml", host);
|
||||
VBox body = body(table(loaded));
|
||||
assertEquals(80, ((Region) body.getChildren().get(0)).getPrefHeight());
|
||||
assertEquals(24, ((Region) body.getChildren().get(1)).getPrefHeight());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A row whose fields are swapped still loads.
|
||||
*/
|
||||
@Test
|
||||
void aRowWhoseFieldsAreSwappedStillLoads() throws Exception {
|
||||
onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Host host = host(main);
|
||||
LoadedView loaded = load("show-visits-swapped.fxml", host);
|
||||
VBox body = body(table(loaded));
|
||||
assertEquals(1, body.getChildren().size());
|
||||
CityFirstRow row = (CityFirstRow) body.getChildren().get(0);
|
||||
assertSame(main, row.getModel());
|
||||
assertEquals("Berlin", field(row, "cityField").getText());
|
||||
assertEquals("Main", field(row, "streetField").getText());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding and removing a visit updates the table and the entity.
|
||||
*/
|
||||
@Test
|
||||
void addingAndRemovingAVisitUpdatesTheTableAndTheEntity() throws Exception {
|
||||
onFx(() -> {
|
||||
Visit main = new Visit("Main", "Berlin");
|
||||
Visit oak = new Visit("Oak", "Bonn");
|
||||
Host host = host(main, oak);
|
||||
LoadedView loaded = load("show-visits.fxml", host);
|
||||
@SuppressWarnings("unchecked")
|
||||
ObservableList<Visit> visits = (ObservableList<Visit>) loaded.property("visits").getValue();
|
||||
Visit elm = new Visit("Elm", "Hamburg");
|
||||
visits.add(elm);
|
||||
VBox body = body(table(loaded));
|
||||
assertEquals(3, body.getChildren().size());
|
||||
assertSame(elm, ((VisitRow) body.getChildren().get(2)).getModel());
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(host.getVisits()));
|
||||
visits.remove(oak);
|
||||
assertEquals(2, body.getChildren().size());
|
||||
assertSame(main, ((VisitRow) body.getChildren().get(0)).getModel());
|
||||
assertSame(elm, ((VisitRow) body.getChildren().get(1)).getModel());
|
||||
assertEquals(List.of("Main", "Elm"), streets(host.getVisits()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A row scrolled out of sight still exists, and the header stays put.
|
||||
*/
|
||||
@Test
|
||||
void aRowScrolledOutOfSightStillExists() throws Exception {
|
||||
onFx(() -> {
|
||||
List<Visit> visits = new ArrayList<>();
|
||||
for (int index = 0; index < 12; index++) {
|
||||
visits.add(new Visit("Street " + index, "Berlin"));
|
||||
}
|
||||
Host host = new Host();
|
||||
host.setVisits(visits);
|
||||
LoadedView loaded = load("show-visits.fxml", host);
|
||||
Table table = table(loaded);
|
||||
VBox body = body(table);
|
||||
assertEquals(12, body.getChildren().size());
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
assertSame(body, scroll.getContent());
|
||||
HBox header = header(table);
|
||||
assertFalse(scroll.getContent() instanceof Parent parent && parent.getChildrenUnmodifiable().contains(header));
|
||||
assertSame(body, scroll.getContent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty collection shows the headers and a blank body.
|
||||
*/
|
||||
@Test
|
||||
void anEmptyCollectionShowsTheHeadersAndABlankBody() throws Exception {
|
||||
onFx(() -> {
|
||||
Host host = new Host();
|
||||
LoadedView loaded = load("show-visits.fxml", host);
|
||||
Table table = table(loaded);
|
||||
assertEquals(2, header(table).getChildren().size());
|
||||
assertEquals(0, body(table).getChildren().size());
|
||||
assertNull(body(table).lookup(".label"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A table with no column fails to load.
|
||||
*/
|
||||
@Test
|
||||
void aTableWithNoColumnFailsToLoad() throws Exception {
|
||||
onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"));
|
||||
assertThrows(IllegalArgumentException.class, () -> load("show-visits-no-column.fxml", host));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A table that names one attribute twice fails to load.
|
||||
*/
|
||||
@Test
|
||||
void aRepeatedAttributeFailsToLoad() throws Exception {
|
||||
onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"));
|
||||
assertThrows(IllegalArgumentException.class, () -> load("show-visits-duplicate.fxml", host));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A column that is not a simple value fails to load.
|
||||
*/
|
||||
@Test
|
||||
void aComplexAttributeFailsToLoad() throws Exception {
|
||||
onFx(() -> {
|
||||
Host host = host(new Visit("Main", "Berlin"));
|
||||
assertThrows(IllegalArgumentException.class, () -> load("show-visits-complex.fxml", host));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a view of visits.
|
||||
*
|
||||
* @param name the document name
|
||||
* @param host the model
|
||||
* @return the loaded view
|
||||
*/
|
||||
private static LoadedView load(String name, Host host) {
|
||||
URL fxml = ShowATableOfRowsTest.class.getResource(name);
|
||||
assertNotNull(fxml);
|
||||
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.component");
|
||||
return ViewLoader.load(fxml, host, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a host whose visits are {@code visits}.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the host
|
||||
*/
|
||||
private static Host host(Visit... visits) {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(visits)));
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table in a loaded view.
|
||||
*
|
||||
* @param loaded the loaded view
|
||||
* @return the table
|
||||
*/
|
||||
private static Table table(LoadedView loaded) {
|
||||
return (Table) loaded.getRoot().lookup("#visitsTable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header row.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the header
|
||||
*/
|
||||
private static HBox header(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
return (HBox) headerScroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header text of one Column.
|
||||
*
|
||||
* @param header the header row
|
||||
* @param index the Column index
|
||||
* @return the header text
|
||||
*/
|
||||
private static String headerText(HBox header, int index) {
|
||||
Label label = (Label) header.getChildren().get(index).lookup("." + Table.HEADER_TEXT_STYLE);
|
||||
return label.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body that holds the rows.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the body
|
||||
*/
|
||||
private static VBox body(Table table) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
assertInstanceOf(VBox.class, scroll.getContent());
|
||||
return (VBox) scroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a named field in a row.
|
||||
*
|
||||
* @param row the row
|
||||
* @param id the field id
|
||||
* @return the field
|
||||
*/
|
||||
private static TextField field(Parent row, String id) {
|
||||
return (TextField) row.lookup("#" + id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the visits, in list order.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(List<Visit> visits) {
|
||||
return visits.stream().map(Visit::getStreet).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one check on the JavaFX application thread.
|
||||
*
|
||||
* @param action the check
|
||||
*/
|
||||
private static void onFx(FxAction action) throws Exception {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
return;
|
||||
}
|
||||
AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
action.run();
|
||||
} catch (Throwable failure) {
|
||||
thrown.set(failure);
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
if (!done.await(10, TimeUnit.SECONDS)) {
|
||||
fail("JavaFX thread did not finish");
|
||||
}
|
||||
Throwable failure = thrown.get();
|
||||
if (failure instanceof Exception exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One check that runs on the JavaFX application thread.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FxAction {
|
||||
/**
|
||||
* Runs the check.
|
||||
*
|
||||
* @throws Exception when the check fails
|
||||
*/
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package de.neitzel.fx.view;
|
||||
|
||||
import de.neitzel.fx.view.component.VisitRow;
|
||||
import de.neitzel.fx.view.fixture.Host;
|
||||
import de.neitzel.fx.view.fixture.Role;
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import de.neitzel.injection.ApplicationContext;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* A header click sorts one Column without rearranging the collection.
|
||||
*/
|
||||
class SortOneColumnTest {
|
||||
|
||||
/**
|
||||
* Starts the JavaFX toolkit once for this class.
|
||||
*/
|
||||
@BeforeAll
|
||||
static void startJavaFx() throws InterruptedException {
|
||||
FxThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* A header click switches direction and drops the other Column's Sort.
|
||||
*/
|
||||
@Test
|
||||
void aHeaderClickSortsOneColumn() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit main = new Visit("Main", "Bonn");
|
||||
Visit oak = new Visit("Oak", "Berlin");
|
||||
Visit elm = new Visit("Elm", null);
|
||||
Host host = host(main, oak, elm);
|
||||
LoadedView loaded = load("show-two-tables.fxml", host);
|
||||
Table first = (Table) loaded.getRoot().lookup("#firstTable");
|
||||
clickHeader(first, 1);
|
||||
assertTrue(mark(first, 1).isVisible());
|
||||
assertEquals(List.of("Oak", "Main", "Elm"), streets(first));
|
||||
assertEquals("ascending", mark(first, 1).getText());
|
||||
assertFalse(mark(first, 0).isVisible());
|
||||
clickHeader(first, 1);
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(first));
|
||||
assertEquals("descending", mark(first, 1).getText());
|
||||
clickHeader(first, 0);
|
||||
assertEquals(List.of("Elm", "Main", "Oak"), streets(first));
|
||||
assertTrue(mark(first, 0).isVisible());
|
||||
assertFalse(mark(first, 1).isVisible());
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(host.getVisits()));
|
||||
Table second = (Table) loaded.getRoot().lookup("#secondTable");
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(second));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The popup sorts and clears, and a click outside leaves the Sort.
|
||||
*/
|
||||
@Test
|
||||
void thePopupSortsAndClears() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit berlin = new Visit("Main", "berlin");
|
||||
Visit capital = new Visit("Oak", "Berlin");
|
||||
Visit aachen = new Visit("Elm", "Aachen");
|
||||
Host host = host(berlin, capital, aachen);
|
||||
Table table = (Table) load("show-two-tables.fxml", host).getRoot().lookup("#firstTable");
|
||||
opener(table, 1).fire();
|
||||
VBox popup = popup(table);
|
||||
assertTrue(popup.isVisible());
|
||||
command(popup, "Ascending").fire();
|
||||
assertFalse(popup.isVisible());
|
||||
assertEquals(List.of("Elm", "Main", "Oak"), streets(table));
|
||||
opener(table, 1).fire();
|
||||
table.fireEvent(click());
|
||||
assertFalse(popup(table).isVisible());
|
||||
assertEquals(List.of("Elm", "Main", "Oak"), streets(table));
|
||||
opener(table, 1).fire();
|
||||
command(popup(table), "Clear sort").fire();
|
||||
assertEquals(List.of("Main", "Oak", "Elm"), streets(table));
|
||||
assertFalse(mark(table, 1).isVisible());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Numbers, booleans, enums, and dates sort in their own order. Missing values stay last.
|
||||
*/
|
||||
@Test
|
||||
void eachTypeHasItsOwnOrder() throws Exception {
|
||||
FxThread.onFx(() -> {
|
||||
Visit low = visit("Low", 1, false, Role.ADMIN, LocalDate.of(2020, 1, 2));
|
||||
Visit high = visit("High", 10, true, Role.MEMBER, LocalDate.of(2019, 5, 1));
|
||||
Visit missing = visit("Missing", null, false, null, null);
|
||||
Host host = host(high, missing, low);
|
||||
Table table = (Table) load("show-two-tables.fxml", host).getRoot().lookup("#firstTable");
|
||||
clickHeader(table, 2);
|
||||
assertEquals(List.of("Low", "High", "Missing"), streets(table));
|
||||
clickHeader(table, 3);
|
||||
assertEquals(List.of("Missing", "Low", "High"), streets(table));
|
||||
clickHeader(table, 4);
|
||||
assertEquals(List.of("High", "Low", "Missing"), streets(table));
|
||||
clickHeader(table, 5);
|
||||
assertEquals(List.of("High", "Low", "Missing"), streets(table));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a visit with the extra Attributes filled in.
|
||||
*
|
||||
* @param street the street
|
||||
* @param rank the rank
|
||||
* @param open whether the visit is open
|
||||
* @param role the role
|
||||
* @param day the day
|
||||
* @return the visit
|
||||
*/
|
||||
private static Visit visit(String street, Integer rank, boolean open, Role role, LocalDate day) {
|
||||
Visit visit = new Visit(street, "Berlin");
|
||||
visit.setRank(rank);
|
||||
visit.setOpen(open);
|
||||
visit.setRole(role);
|
||||
visit.setDay(day);
|
||||
return visit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a document.
|
||||
*
|
||||
* @param name the document name
|
||||
* @param host the model
|
||||
* @return the loaded view
|
||||
*/
|
||||
private static LoadedView load(String name, Host host) {
|
||||
URL fxml = SortOneColumnTest.class.getResource(name);
|
||||
assertNotNull(fxml);
|
||||
return ViewLoader.load(fxml, host, new ApplicationContext("de.neitzel.fx.view.component"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a host with those visits.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the host
|
||||
*/
|
||||
private static Host host(Visit... visits) {
|
||||
Host host = new Host();
|
||||
host.setVisits(new ArrayList<>(List.of(visits)));
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a Column header.
|
||||
*
|
||||
* @param table the table
|
||||
* @param index the Column index
|
||||
*/
|
||||
private static void clickHeader(Table table, int index) {
|
||||
HBox header = header(table);
|
||||
header.getChildren().get(index).fireEvent(click());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header row.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the header
|
||||
*/
|
||||
private static HBox header(Table table) {
|
||||
ScrollPane headerScroll = (ScrollPane) table.lookup(".table-header-scroll");
|
||||
return (HBox) headerScroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sort mark of one Column.
|
||||
*
|
||||
* @param table the table
|
||||
* @param index the Column index
|
||||
* @return the mark
|
||||
*/
|
||||
private static Label mark(Table table, int index) {
|
||||
return (Label) header(table).getChildren().get(index).lookup("." + Table.SORT_MARK_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the opener of one Column.
|
||||
*
|
||||
* @param table the table
|
||||
* @param index the Column index
|
||||
* @return the opener
|
||||
*/
|
||||
private static Button opener(Table table, int index) {
|
||||
return (Button) header(table).getChildren().get(index).lookup("." + Table.OPENER_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the popup.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the popup
|
||||
*/
|
||||
private static VBox popup(Table table) {
|
||||
return (VBox) table.lookup("." + Table.POPUP_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a popup command.
|
||||
*
|
||||
* @param popup the popup
|
||||
* @param text the command text
|
||||
* @return the button
|
||||
*/
|
||||
private static Button command(VBox popup, String text) {
|
||||
return popup.getChildren().stream()
|
||||
.filter(Button.class::isInstance)
|
||||
.map(Button.class::cast)
|
||||
.filter(button -> text.equals(button.getText()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the visible rows.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(Table table) {
|
||||
return body(table).getChildren().stream()
|
||||
.map(node -> ((javafx.scene.control.TextField) ((javafx.scene.Parent) node).lookup("#streetField"))
|
||||
.getText())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the streets of the collection.
|
||||
*
|
||||
* @param visits the visits
|
||||
* @return the streets
|
||||
*/
|
||||
private static List<String> streets(List<Visit> visits) {
|
||||
return visits.stream().map(Visit::getStreet).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body.
|
||||
*
|
||||
* @param table the table
|
||||
* @return the body
|
||||
*/
|
||||
private static VBox body(Table table) {
|
||||
ScrollPane scroll = (ScrollPane) table.lookup("." + Table.SCROLL_STYLE);
|
||||
return (VBox) scroll.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a primary click.
|
||||
*
|
||||
* @return the event
|
||||
*/
|
||||
private static MouseEvent click() {
|
||||
return new MouseEvent(MouseEvent.MOUSE_CLICKED, 1, 1, 1, 1, MouseButton.PRIMARY, 1,
|
||||
false, false, false, false, true, false, false, false, false, false, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.neitzel.fx.view.component;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
/**
|
||||
* A Row that lays city out before street.
|
||||
*/
|
||||
@Singleton
|
||||
public class CityFirstRow extends HBox {
|
||||
|
||||
/**
|
||||
* The visit this row shows.
|
||||
*/
|
||||
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||
|
||||
/**
|
||||
* Creates a row.
|
||||
*/
|
||||
@Inject
|
||||
public CityFirstRow() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model property.
|
||||
*
|
||||
* @return the model property
|
||||
*/
|
||||
public ObjectProperty<Object> modelProperty() {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visit this row shows.
|
||||
*
|
||||
* @return the visit, or {@code null}
|
||||
*/
|
||||
public Object getModel() {
|
||||
return model.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.neitzel.fx.view.component;
|
||||
|
||||
import de.neitzel.fx.view.fixture.Visit;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
/**
|
||||
* A Row that shows a visit's street and city, street first.
|
||||
*/
|
||||
@Singleton
|
||||
public class VisitRow extends HBox {
|
||||
|
||||
/**
|
||||
* The visit this row shows.
|
||||
*/
|
||||
private final ObjectProperty<Object> model = new SimpleObjectProperty<>(this, "model");
|
||||
|
||||
/**
|
||||
* The street field, bound by this row.
|
||||
*/
|
||||
@FXML
|
||||
private TextField streetField;
|
||||
|
||||
/**
|
||||
* Creates a row.
|
||||
*/
|
||||
@Inject
|
||||
public VisitRow() {
|
||||
model.addListener((observable, previous, current) -> applyHeight(current));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives a visit named Tall a taller row. Every other visit stays one line.
|
||||
*
|
||||
* @param current the model, or {@code null}
|
||||
*/
|
||||
private void applyHeight(Object current) {
|
||||
if (current instanceof Visit visit && "Tall".equals(visit.getStreet())) {
|
||||
setPrefHeight(80);
|
||||
} else {
|
||||
setPrefHeight(24);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model property.
|
||||
*
|
||||
* @return the model property
|
||||
*/
|
||||
public ObjectProperty<Object> modelProperty() {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visit this row shows.
|
||||
*
|
||||
* @return the visit, or {@code null}
|
||||
*/
|
||||
public Object getModel() {
|
||||
return model.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visit this row shows.
|
||||
*
|
||||
* @param model the visit
|
||||
*/
|
||||
public void setModel(Object model) {
|
||||
this.model.set(model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.neitzel.fx.view.fixture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A model whose visits a Table shows.
|
||||
*/
|
||||
public class Host {
|
||||
|
||||
/**
|
||||
* The visits. After a view loads, this is the list the Table watches.
|
||||
*/
|
||||
private List<Visit> visits = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The selected visit, or {@code null} when none is selected.
|
||||
*/
|
||||
private Visit selectedVisit;
|
||||
|
||||
/**
|
||||
* Returns the visits.
|
||||
*
|
||||
* @return the visits
|
||||
*/
|
||||
public List<Visit> getVisits() {
|
||||
return visits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visits.
|
||||
*
|
||||
* @param visits the visits
|
||||
*/
|
||||
public void setVisits(List<Visit> visits) {
|
||||
this.visits = visits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected visit.
|
||||
*
|
||||
* @return the selected visit, or {@code null}
|
||||
*/
|
||||
public Visit getSelectedVisit() {
|
||||
return selectedVisit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected visit.
|
||||
*
|
||||
* @param selectedVisit the selected visit
|
||||
*/
|
||||
public void setSelectedVisit(Visit selectedVisit) {
|
||||
this.selectedVisit = selectedVisit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package de.neitzel.fx.view.fixture;
|
||||
|
||||
/**
|
||||
* One visit a table can show.
|
||||
*/
|
||||
public class Visit {
|
||||
|
||||
/**
|
||||
* The street.
|
||||
*/
|
||||
private String street;
|
||||
|
||||
/**
|
||||
* The city.
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* A whole number, used when a Column sorts or filters by magnitude.
|
||||
*/
|
||||
private Integer rank;
|
||||
|
||||
/**
|
||||
* Whether the visit is open.
|
||||
*/
|
||||
private boolean open;
|
||||
|
||||
/**
|
||||
* The visitor's role.
|
||||
*/
|
||||
private Role role;
|
||||
|
||||
/**
|
||||
* The day of the visit.
|
||||
*/
|
||||
private java.time.LocalDate day;
|
||||
|
||||
/**
|
||||
* A nested address. A Column cannot use this Attribute.
|
||||
*/
|
||||
private Address place;
|
||||
|
||||
/**
|
||||
* Creates a visit.
|
||||
*
|
||||
* @param street the street
|
||||
* @param city the city
|
||||
*/
|
||||
public Visit(String street, String city) {
|
||||
this.street = street;
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the street.
|
||||
*
|
||||
* @return the street
|
||||
*/
|
||||
public String getStreet() {
|
||||
return street;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the street.
|
||||
*
|
||||
* @param street the street
|
||||
*/
|
||||
public void setStreet(String street) {
|
||||
this.street = street;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the city.
|
||||
*
|
||||
* @return the city
|
||||
*/
|
||||
public String getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the city.
|
||||
*
|
||||
* @param city the city
|
||||
*/
|
||||
public void setCity(String city) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rank.
|
||||
*
|
||||
* @return the rank, or {@code null}
|
||||
*/
|
||||
public Integer getRank() {
|
||||
return rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rank.
|
||||
*
|
||||
* @param rank the rank
|
||||
*/
|
||||
public void setRank(Integer rank) {
|
||||
this.rank = rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the visit is open.
|
||||
*
|
||||
* @return {@code true} when the visit is open
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the visit is open.
|
||||
*
|
||||
* @param open whether the visit is open
|
||||
*/
|
||||
public void setOpen(boolean open) {
|
||||
this.open = open;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the role.
|
||||
*
|
||||
* @return the role, or {@code null}
|
||||
*/
|
||||
public Role getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the role.
|
||||
*
|
||||
* @param role the role
|
||||
*/
|
||||
public void setRole(Role role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the day.
|
||||
*
|
||||
* @return the day, or {@code null}
|
||||
*/
|
||||
public java.time.LocalDate getDay() {
|
||||
return day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the day.
|
||||
*
|
||||
* @param day the day
|
||||
*/
|
||||
public void setDay(java.time.LocalDate day) {
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nested address.
|
||||
*
|
||||
* @return the nested address, or {@code null}
|
||||
*/
|
||||
public Address getPlace() {
|
||||
return place;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the nested address.
|
||||
*
|
||||
* @param place the nested address
|
||||
*/
|
||||
public void setPlace(Address place) {
|
||||
this.place = place;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import de.neitzel.fx.view.BindingList?>
|
||||
<?import de.neitzel.fx.view.DirectBinding?>
|
||||
|
||||
<fx:root type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
<fx:define>
|
||||
<BindingList>
|
||||
<DirectBinding control="cityField" property="text" path="city" direction="both"/>
|
||||
<DirectBinding control="streetField" property="text" path="street" direction="both"/>
|
||||
</BindingList>
|
||||
</fx:define>
|
||||
<TextField fx:id="cityField" id="cityField"/>
|
||||
<TextField fx:id="streetField" id="streetField"/>
|
||||
</fx:root>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import de.neitzel.fx.view.BindingList?>
|
||||
<?import de.neitzel.fx.view.DirectBinding?>
|
||||
|
||||
<fx:root type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
<fx:define>
|
||||
<BindingList>
|
||||
<DirectBinding control="streetField" property="text" path="street" direction="both"/>
|
||||
<DirectBinding control="cityField" property="text" path="city" direction="both"/>
|
||||
</BindingList>
|
||||
</fx:define>
|
||||
<TextField fx:id="streetField" id="streetField"/>
|
||||
<TextField fx:id="cityField" id="cityField"/>
|
||||
</fx:root>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="firstTable" id="firstTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street"/>
|
||||
<Column header="City" attribute="city"/>
|
||||
<Column header="Rank" attribute="rank"/>
|
||||
<Column header="Open" attribute="open"/>
|
||||
<Column header="Role" attribute="role"/>
|
||||
<Column header="Day" attribute="day"/>
|
||||
</Table>
|
||||
<Table fx:id="secondTable" id="secondTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="City" attribute="city"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow">
|
||||
<Column header="Place" attribute="place"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow">
|
||||
<Column header="Street" attribute="street"/>
|
||||
<Column header="Street again" attribute="street"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow"/>
|
||||
</VBox>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.BindingList?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.DirectBinding?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<fx:define>
|
||||
<BindingList>
|
||||
<DirectBinding control="visitsTable" property="selection" path="selectedVisit" direction="both"/>
|
||||
</BindingList>
|
||||
</fx:define>
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300" prefHeight="200">
|
||||
<Column header="Street" attribute="street"/>
|
||||
<Column header="City" attribute="city"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.CityFirstRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street"/>
|
||||
<Column header="City" attribute="city"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import de.neitzel.fx.view.Column?>
|
||||
<?import de.neitzel.fx.view.Table?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<Table fx:id="visitsTable" id="visitsTable" collection="visits"
|
||||
row="de.neitzel.fx.view.component.VisitRow" prefWidth="300">
|
||||
<Column header="Street" attribute="street"/>
|
||||
<Column header="City" attribute="city"/>
|
||||
</Table>
|
||||
</VBox>
|
||||
Reference in New Issue
Block a user