19 package org.sleuthkit.autopsy.timeline.ui.listvew;
 
   21 import com.google.common.collect.Iterables;
 
   22 import com.google.common.math.DoubleMath;
 
   23 import java.math.RoundingMode;
 
   24 import java.time.Instant;
 
   25 import java.time.ZoneId;
 
   26 import java.time.ZonedDateTime;
 
   27 import java.time.temporal.ChronoField;
 
   28 import java.time.temporal.TemporalUnit;
 
   29 import java.util.ArrayList;
 
   30 import java.util.Arrays;
 
   31 import java.util.Collection;
 
   32 import java.util.Collections;
 
   33 import java.util.Comparator;
 
   34 import java.util.List;
 
   35 import java.util.Objects;
 
   37 import java.util.SortedSet;
 
   38 import java.util.TreeSet;
 
   39 import java.util.concurrent.ConcurrentSkipListSet;
 
   40 import java.util.function.Consumer;
 
   41 import java.util.function.Function;
 
   42 import java.util.logging.Level;
 
   43 import java.util.stream.Collectors;
 
   44 import javafx.application.Platform;
 
   45 import javafx.beans.binding.Bindings;
 
   46 import javafx.beans.binding.IntegerBinding;
 
   47 import javafx.beans.binding.StringBinding;
 
   48 import javafx.beans.property.SimpleObjectProperty;
 
   49 import javafx.beans.value.ObservableValue;
 
   50 import javafx.collections.ListChangeListener;
 
   51 import javafx.event.ActionEvent;
 
   52 import javafx.fxml.FXML;
 
   53 import javafx.geometry.Pos;
 
   54 import javafx.scene.Node;
 
   55 import javafx.scene.control.Button;
 
   56 import javafx.scene.control.ComboBox;
 
   57 import javafx.scene.control.ContextMenu;
 
   58 import javafx.scene.control.Label;
 
   59 import javafx.scene.control.MenuItem;
 
   60 import javafx.scene.control.OverrunStyle;
 
   61 import javafx.scene.control.SelectionMode;
 
   62 import javafx.scene.control.SeparatorMenuItem;
 
   63 import javafx.scene.control.TableCell;
 
   64 import javafx.scene.control.TableColumn;
 
   65 import javafx.scene.control.TableRow;
 
   66 import javafx.scene.control.TableView;
 
   67 import javafx.scene.control.Tooltip;
 
   68 import javafx.scene.image.Image;
 
   69 import javafx.scene.image.ImageView;
 
   70 import javafx.scene.layout.BorderPane;
 
   71 import javafx.scene.layout.HBox;
 
   72 import javafx.scene.layout.VBox;
 
   73 import javafx.util.Callback;
 
   74 import javax.swing.Action;
 
   75 import javax.swing.JMenuItem;
 
   76 import org.controlsfx.control.Notifications;
 
   77 import org.controlsfx.control.action.ActionUtils;
 
   78 import org.openide.awt.Actions;
 
   79 import org.openide.util.NbBundle;
 
   80 import org.openide.util.actions.Presenter;
 
  103 class ListTimeline 
extends BorderPane {
 
  107     private static final Image HASH_HIT = 
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");  
 
  108     private static final Image TAG = 
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");  
 
  109     private static final Image FIRST = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_first.png");  
 
  110     private static final Image PREVIOUS = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_previous.png");  
 
  111     private static final Image NEXT = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_next.png");  
 
  112     private static final Image LAST = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_last.png");  
 
  117     private static final Callback<TableColumn.CellDataFeatures<
CombinedEvent, 
CombinedEvent>, ObservableValue<CombinedEvent>> CELL_VALUE_FACTORY = param -> 
new SimpleObjectProperty<>(param.getValue());
 
  119     private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
 
  121             ChronoField.MONTH_OF_YEAR,
 
  122             ChronoField.DAY_OF_MONTH,
 
  123             ChronoField.HOUR_OF_DAY,
 
  124             ChronoField.MINUTE_OF_HOUR,
 
  125             ChronoField.SECOND_OF_MINUTE);
 
  127     private static final int DEFAULT_ROW_HEIGHT = 24;
 
  130     private HBox navControls;
 
  133     private ComboBox<ChronoField> scrollInrementComboBox;
 
  136     private Button firstButton;
 
  139     private Button previousButton;
 
  142     private Button nextButton;
 
  145     private Button lastButton;
 
  148     private Label eventCountLabel;
 
  150     private TableView<CombinedEvent> table;
 
  152     private TableColumn<CombinedEvent, CombinedEvent> idColumn;
 
  154     private TableColumn<CombinedEvent, CombinedEvent> dateTimeColumn;
 
  156     private TableColumn<CombinedEvent, CombinedEvent> descriptionColumn;
 
  158     private TableColumn<CombinedEvent, CombinedEvent> typeColumn;
 
  160     private TableColumn<CombinedEvent, CombinedEvent> knownColumn;
 
  162     private TableColumn<CombinedEvent, CombinedEvent> taggedColumn;
 
  164     private TableColumn<CombinedEvent, CombinedEvent> hashHitColumn;
 
  170     private final SortedSet<CombinedEvent> visibleEvents;
 
  173     private final SleuthkitCase sleuthkitCase;
 
  181     private final ListChangeListener<CombinedEvent> selectedEventListener = 
new ListChangeListener<CombinedEvent>() {
 
  183         public void onChanged(ListChangeListener.Change<? extends CombinedEvent> c) {
 
  184             controller.
selectEventIDs(table.getSelectionModel().getSelectedItems().stream()
 
  185                     .filter(Objects::nonNull)
 
  187                     .collect(Collectors.toSet()));
 
  197         this.controller = controller;
 
  201         this.visibleEvents = 
new ConcurrentSkipListSet<>(Comparator.comparing(table.getItems()::indexOf));
 
  206         "# {0} - the number of events",
 
  207         "ListTimeline.eventCountLabel.text={0} events"})
 
  209         assert eventCountLabel != null : 
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  210         assert table != null : 
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  211         assert idColumn != null : 
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  212         assert dateTimeColumn != null : 
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  213         assert descriptionColumn != null : 
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  214         assert typeColumn != null : 
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  215         assert knownColumn != null : 
"fx:id=\"knownColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  220         scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
 
  221         scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
 
  222         ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
 
  223         ActionUtils.configureButton(
new ScrollToPrevious(), previousButton);
 
  224         ActionUtils.configureButton(
new ScrollToNext(), nextButton);
 
  225         ActionUtils.configureButton(
new ScrollToLast(), lastButton);
 
  228         table.setRowFactory(tableView -> 
new EventRow());
 
  231         table.getColumns().remove(idColumn);
 
  234         dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  235         dateTimeColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  238         descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  239         descriptionColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  242         typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  243         typeColumn.setCellFactory(col -> 
new EventTypeCell());
 
  245         knownColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  246         knownColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  247                 singleEvent.getKnown().getName()));
 
  249         taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  250         taggedColumn.setCellFactory(col -> 
new TaggedCell());
 
  252         hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  253         hashHitColumn.setCellFactory(col -> 
new HashHitCell());
 
  256         eventCountLabel.textProperty().bind(
new StringBinding() {
 
  258                 bind(table.getItems());
 
  262             protected String computeValue() {
 
  263                 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
 
  268         table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
 
  269         table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
 
  279     void setCombinedEvents(Collection<CombinedEvent> events) {
 
  280         table.getItems().setAll(events);
 
  288     void selectEvents(Collection<Long> selectedEventIDs) {
 
  289         if (selectedEventIDs.isEmpty()) {
 
  291             table.getSelectionModel().clearSelection();
 
  303             table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
 
  305             table.getSelectionModel().clearSelection();
 
  307             table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
 
  310             int[] selectedIndices = table.getItems().stream()
 
  311                     .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) == 
false)
 
  312                     .mapToInt(table.getItems()::indexOf)
 
  316             if (selectedIndices.length > 0) {
 
  317                 Integer firstSelectedIndex = selectedIndices[0];
 
  318                 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
 
  319                 scrollTo(firstSelectedIndex);
 
  320                 table.requestFocus(); 
 
  332     List<Node> getTimeNavigationControls() {
 
  333         return Collections.singletonList(navControls);
 
  343     private void scrollToAndFocus(Integer index) {
 
  344         table.requestFocus();
 
  346         table.getFocusModel().focus(index);
 
  354     private void scrollTo(Integer index) {
 
  355         if (visibleEvents.contains(table.getItems().get(index)) == 
false) {
 
  356             table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
 
  366             "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
 
  367             "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
 
  368             "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
 
  369             "ListView.EventTypeCell.changedTooltip=File Changed ( C )" 
  372         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  373             super.updateItem(item, empty);
 
  375             if (empty || item == null) {
 
  381                     String typeString = 
""; 
 
  382                     VBox toolTipVbox = 
new VBox(5);
 
  384                     for (FileSystemTypes type : Arrays.asList(FileSystemTypes.FILE_MODIFIED, FileSystemTypes.FILE_ACCESSED, FileSystemTypes.FILE_CHANGED, FileSystemTypes.FILE_CREATED)) {
 
  389                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(), 
new ImageView(type.getFXImage())));
 
  393                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(), 
new ImageView(type.getFXImage())));
 
  397                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(), 
new ImageView(type.getFXImage())));
 
  401                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(), 
new ImageView(type.getFXImage())));
 
  404                                     throw new UnsupportedOperationException(
"Unknown FileSystemType: " + type.name()); 
 
  412                     Tooltip tooltip = 
new Tooltip();
 
  413                     tooltip.setGraphic(toolTipVbox);
 
  419                     setGraphic(
new ImageView(eventType.
getFXImage()));
 
  435             setAlignment(Pos.CENTER);
 
  439             "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
 
  441             "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
 
  443         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  444             super.updateItem(item, empty);
 
  446             if (empty || item == null || (getEvent().isTagged() == 
false)) {
 
  454                 setGraphic(
new ImageView(TAG));
 
  456                 SortedSet<String> tagNames = 
new TreeSet<>();
 
  459                     AbstractFile abstractFileById = sleuthkitCase.getAbstractFileById(getEvent().getFileID());
 
  461                             .map(tag -> tag.getName().getDisplayName())
 
  462                             .forEach(tagNames::add);
 
  464                 } 
catch (TskCoreException ex) {
 
  465                     LOGGER.log(Level.SEVERE, 
"Failed to lookup tags for obj id " + getEvent().getFileID(), ex); 
 
  466                     Platform.runLater(() -> {
 
  467                         Notifications.create()
 
  468                                 .owner(getScene().getWindow())
 
  469                                 .text(Bundle.ListTimeline_taggedTooltip_error())
 
  476                         BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
 
  478                                 .map(tag -> tag.getName().getDisplayName())
 
  479                                 .forEach(tagNames::add);
 
  480                     } 
catch (TskCoreException ex) {
 
  481                         LOGGER.log(Level.SEVERE, 
"Failed to lookup tags for artifact id " + artifactID, ex); 
 
  482                         Platform.runLater(() -> {
 
  483                             Notifications.create()
 
  484                                     .owner(getScene().getWindow())
 
  485                                     .text(Bundle.ListTimeline_taggedTooltip_error())
 
  490                 Tooltip tooltip = 
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames))); 
 
  491                 tooltip.setGraphic(
new ImageView(TAG));
 
  507             setAlignment(Pos.CENTER);
 
  511             "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
 
  512             "# {0} - hash set names",
 
  513             "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
 
  515         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  516             super.updateItem(item, empty);
 
  518             if (empty || item == null || (getEvent().isHashHit() == 
false)) {
 
  527                 setGraphic(
new ImageView(HASH_HIT));
 
  529                     Set<String> hashSetNames = 
new TreeSet<>(sleuthkitCase.getAbstractFileById(getEvent().getFileID()).getHashSetNames());
 
  530                     Tooltip tooltip = 
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames))); 
 
  531                     tooltip.setGraphic(
new ImageView(HASH_HIT));
 
  533                 } 
catch (TskCoreException ex) {
 
  534                     LOGGER.log(Level.SEVERE, 
"Failed to lookup hash set names for obj id " + getEvent().getFileID(), ex); 
 
  535                     Platform.runLater(() -> {
 
  536                         Notifications.create()
 
  537                                 .owner(getScene().getWindow())
 
  538                                 .text(Bundle.ListTimeline_hashHitTooltip_error())
 
  561             setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
 
  562             setEllipsisString(
" ... "); 
 
  566         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  567             super.updateItem(item, empty);
 
  568             if (empty || item == null) {
 
  572                 String text = textSupplier.apply(getEvent());
 
  574                 setTooltip(
new Tooltip(text));
 
  583     private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
 
  597         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  598             super.updateItem(item, empty);
 
  600             if (empty || item == null) {
 
  612     private class EventRow extends TableRow<CombinedEvent> {
 
  626             "ListChart.errorMsg=There was a problem getting the content for the selected event."})
 
  628         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  629             CombinedEvent oldItem = getItem();
 
  630             if (oldItem != null) {
 
  631                 visibleEvents.remove(oldItem);
 
  633             super.updateItem(item, empty);
 
  635             if (empty || item == null) {
 
  638                 visibleEvents.add(item);
 
  641                 setOnContextMenuRequested(contextMenuEvent -> {
 
  645                         List<MenuItem> menuItems = 
new ArrayList<>();
 
  648                         for (Action action : node.
getActions(
false)) {
 
  649                             if (action == null) {
 
  651                                 menuItems.add(
new SeparatorMenuItem());
 
  653                                 String actionName = Objects.toString(action.getValue(Action.NAME));
 
  655                                 if (Arrays.asList(
"&Properties", 
"Tools").contains(actionName) == 
false) { 
 
  656                                     if (action instanceof Presenter.Popup) {
 
  663                                         JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
 
  664                                         menuItems.add(SwingFXMenuUtils.createFXMenu(submenu));
 
  666                                         menuItems.add(SwingFXMenuUtils.createFXMenu(
new Actions.MenuItem(action, 
false)));
 
  673                         new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
 
  674                                 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
 
  677                         LOGGER.log(Level.SEVERE, 
"There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); 
 
  678                     } 
catch (TskCoreException ex) {
 
  679                         LOGGER.log(Level.SEVERE, 
"Failed to lookup Sleuthkit object backing a SingleEvent.", ex); 
 
  680                         Platform.runLater(() -> {
 
  681                             Notifications.create()
 
  682                                     .owner(getScene().getWindow())
 
  683                                     .text(Bundle.ListChart_errorMsg())
 
  696             super(
"", 
new Consumer<ActionEvent>() { 
 
  698                 public void accept(ActionEvent actionEvent) {
 
  702             setGraphic(
new ImageView(FIRST));
 
  703             disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
 
  710             super(
"", 
new Consumer<ActionEvent>() {  
 
  712                 public void accept(ActionEvent actionEvent) {
 
  713                     scrollToAndFocus(table.getItems().size() - 1);
 
  716             setGraphic(
new ImageView(LAST));
 
  717             IntegerBinding size = Bindings.size(table.getItems());
 
  718             disabledProperty().bind(size.isEqualTo(0).or(
 
  719                     table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
 
  726             super(
"", 
new Consumer<ActionEvent>() { 
 
  728                 public void accept(ActionEvent actionEvent) {
 
  729                     ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
 
  731                     TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
 
  733                     int focusedIndex = table.getFocusModel().getFocusedIndex();
 
  734                     CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
 
  735                     if (-1 == focusedIndex || null == focusedItem) {
 
  736                         focusedItem = visibleEvents.first();
 
  737                         focusedIndex = table.getItems().indexOf(focusedItem);
 
  740                     ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
 
  741                     ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
 
  742                     for (ChronoField field : SCROLL_BY_UNITS) {
 
  743                         if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
 
  744                             nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
 
  747                     long nextMillis = nextDateTime.toInstant().toEpochMilli();
 
  749                     int nextIndex = table.getItems().size() - 1;
 
  750                     for (
int i = focusedIndex; i < table.getItems().size(); i++) {
 
  751                         if (table.getItems().get(i).getStartMillis() >= nextMillis) {
 
  756                     scrollToAndFocus(nextIndex);
 
  759             setGraphic(
new ImageView(NEXT));
 
  760             IntegerBinding size = Bindings.size(table.getItems());
 
  761             disabledProperty().bind(size.isEqualTo(0).or(
 
  762                     table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
 
  770             super(
"", 
new Consumer<ActionEvent>() { 
 
  772                 public void accept(ActionEvent actionEvent) {
 
  774                     ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
 
  775                     TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
 
  777                     int focusedIndex = table.getFocusModel().getFocusedIndex();
 
  778                     CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
 
  779                     if (-1 == focusedIndex || null == focusedItem) {
 
  780                         focusedItem = visibleEvents.last();
 
  781                         focusedIndex = table.getItems().indexOf(focusedItem);
 
  784                     ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
 
  785                     ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
 
  787                     for (ChronoField field : SCROLL_BY_UNITS) {
 
  788                         if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
 
  789                             previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
 
  792                     long previousMillis = previousDateTime.toInstant().toEpochMilli();
 
  794                     int previousIndex = 0;
 
  795                     for (
int i = focusedIndex; i > 0; i--) {
 
  796                         if (table.getItems().get(i).getStartMillis() <= previousMillis) {
 
  802                     scrollToAndFocus(previousIndex);
 
  805             setGraphic(
new ImageView(PREVIOUS));
 
  806             disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
 
Optional< Long > getArtifactID()
FilteredEventsModel getEventsModel()
static EventNode createEventNode(final Long eventID, FilteredEventsModel eventsModel)
Action[] getActions(boolean context)
synchronized void selectEventIDs(Collection< Long > eventIDs)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
TagsManager getTagsManager()
void updateItem(CombinedEvent item, boolean empty)
Long getRepresentativeEventID()
static ZoneId getTimeZoneID()
final Function< SingleEvent, String > textSupplier
SleuthkitCase getSleuthkitCase()
Set< EventType > getEventTypes()
synchronized ObservableList< Long > getSelectedEventIDs()
synchronized static Logger getLogger(String name)
void updateItem(CombinedEvent item, boolean empty)
SingleEvent getEventById(Long eventID)
static DateTimeFormatter getZonedFormatter()
static void construct(Node node, String fxmlFileName)