package com.insightfullogic.honest_profiler.ports.javafx.controller;
import static com.insightfullogic.honest_profiler.core.aggregation.grouping.CombinedGrouping.combine;
import static com.insightfullogic.honest_profiler.ports.javafx.util.StyleUtil.doubleDiffStyler;
import static com.insightfullogic.honest_profiler.ports.javafx.util.StyleUtil.intDiffStyler;
import static com.insightfullogic.honest_profiler.ports.javafx.util.StyleUtil.longDiffStyler;
import static com.insightfullogic.honest_profiler.ports.javafx.view.Icon.FUNNEL_16;
import static com.insightfullogic.honest_profiler.ports.javafx.view.Icon.FUNNEL_ACTIVE_16;
import static com.insightfullogic.honest_profiler.ports.javafx.view.Icon.viewFor;
import static javafx.scene.input.KeyCode.ENTER;
import com.insightfullogic.honest_profiler.core.aggregation.filter.FilterSpecification;
import com.insightfullogic.honest_profiler.core.aggregation.grouping.CombinedGrouping;
import com.insightfullogic.honest_profiler.core.aggregation.grouping.FrameGrouping;
import com.insightfullogic.honest_profiler.core.aggregation.grouping.ThreadGrouping;
import com.insightfullogic.honest_profiler.core.aggregation.result.ItemType;
import com.insightfullogic.honest_profiler.core.aggregation.result.diff.DiffEntry;
import com.insightfullogic.honest_profiler.ports.javafx.controller.filter.FilterDialogController;
import com.insightfullogic.honest_profiler.ports.javafx.model.ApplicationContext;
import com.insightfullogic.honest_profiler.ports.javafx.model.ProfileContext;
import com.insightfullogic.honest_profiler.ports.javafx.view.cell.CountTableCell;
import com.insightfullogic.honest_profiler.ports.javafx.view.cell.CountTreeTableCell;
import com.insightfullogic.honest_profiler.ports.javafx.view.cell.PercentageTableCell;
import com.insightfullogic.honest_profiler.ports.javafx.view.cell.PercentageTreeTableCell;
import com.insightfullogic.honest_profiler.ports.javafx.view.cell.TimeTableCell;
import com.insightfullogic.honest_profiler.ports.javafx.view.cell.TimeTreeTableCell;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableObjectValue;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.scene.image.ImageView;
/**
* Superclass for all View Controllers in the application. These controllers provide a particular view on data
* consisting of items of type T. The class manages the filter, quickFilter and grouping controls.
* <p>
* This superclass also provides some common UI helper methods for column configuration.
* <p>
*
* @param <T> the type of the items contained in the View which can be filtered
*/
public abstract class AbstractViewController<T> extends AbstractController
{
// Instance Properties
private FilterDialogController<T> filterController;
private Button filterButton;
private Button quickFilterButton;
private TextField quickFilterText;
private Label threadGroupingLabel;
private ChoiceBox<ThreadGrouping> threadGrouping;
private Label frameGroupingLabel;
private ChoiceBox<FrameGrouping> frameGrouping;
private ObjectProperty<FilterSpecification<T>> filterSpec;
private ObjectProperty<CombinedGrouping> grouping;
private ItemType type;
// FXML Implementation
/**
* Initialize method for subclasses which sets the basic properties needed by this superdclass. This method must be
* called by such subclasses in their FXML initialize().
* <p>
*
* @param type the {@link ItemType} of the items shown in the view
*/
protected void initialize(ItemType type)
{
super.initialize();
this.type = type;
}
/**
* Initialize method for subclasses which have filter-related controls, which will be managed by this superclass.
* This method must be called by such subclasses in their FXML initialize(). It should provide the controller-local
* UI nodes needed by the AbstractViewController.
* <p>
*
* @param filterController the {@link FilterDialogController}
* @param filterButton the button used to trigger filter editing
* @param quickFilterButton the button used to apply the quick filter
* @param quickFilterText the TextField providing the value for the quick filter
*/
protected void initialize(FilterDialogController<T> filterController, Button filterButton,
Button quickFilterButton, TextField quickFilterText)
{
this.filterController = filterController;
this.filterButton = filterButton;
this.quickFilterButton = quickFilterButton;
this.quickFilterText = quickFilterText;
// Model initialization
filterSpec = new SimpleObjectProperty<>(new FilterSpecification<>(type));
}
/**
* Initialize method for subclasses which have grouping-related controls, which will be managed by this superclass.
* This method must be called by such subclasses in their FXML initialize(). It should provide the controller-local
* UI nodes needed by the AbstractViewController.
* <p>
*
* @param threadGroupingLabel the label next to the {@link ThreadGrouping} {@link ChoiceBox}
* @param threadGrouping the {@link ThreadGrouping} {@link ChoiceBox}
* @param frameGroupingLabel the label next to the {@link FrameGrouping} {@link ChoiceBox}
* @param frameGrouping the {@link FrameGrouping} {@link ChoiceBox}
*/
protected void initialize(Label threadGroupingLabel, ChoiceBox<ThreadGrouping> threadGrouping,
Label frameGroupingLabel, ChoiceBox<FrameGrouping> frameGrouping)
{
this.threadGroupingLabel = threadGroupingLabel;
this.threadGrouping = threadGrouping;
this.frameGroupingLabel = frameGroupingLabel;
this.frameGrouping = frameGrouping;
// Model initialization
grouping = new SimpleObjectProperty<>();
setVisibility(
false,
threadGroupingLabel,
threadGrouping,
frameGroupingLabel,
frameGrouping);
}
// Instance Accessors
/**
* @see AbstractController#setApplicationContext(ApplicationContext)
*
* @param applicationContext the ApplicationContext of this application
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext)
{
super.setApplicationContext(applicationContext);
// If the subclass doesn't need filter management, no UI control should have been passed on in the initialize()
// method.
if (filterButton == null)
{
return;
}
initializeFilters(applicationContext);
}
// Grouping-related Methods
/**
* Specify which {@link ThreadGrouping}s are allowed for the View. This method should be called by the controller
* which configures the View, if the View supports groupings.
* <p>
*
* @param groupings the {@link ThreadGrouping}s allowed for the View
*/
public void setAllowedThreadGroupings(ThreadGrouping... groupings)
{
threadGrouping.getItems().addAll(groupings);
// Don't show choice if there actually is no choice :)
setVisibility(groupings.length > 1, threadGroupingLabel, threadGrouping);
bindGroupings();
}
/**
* Specify which {@link FrameGrouping}s are allowed for the View. This method should be called by the controller
* which configures the View, if the View supports groupings.
* <p>
*
* @param groupings the {@link FrameGrouping}s allowed for the View
*/
public void setAllowedFrameGroupings(FrameGrouping... groupings)
{
frameGrouping.getItems().addAll(groupings);
// Don't show choice if there actually is no choice :)
setVisibility(groupings.length > 1, frameGroupingLabel, frameGrouping);
bindGroupings();
}
/**
* Returns the property containing the {@link CombinedGrouping} made up by the currently selected
* {@link FrameGrouping} and {@link ThreadGrouping}s.
* <p>
*
* @return the property containing the the {@link CombinedGrouping} made up by the currently selected
* {@link FrameGrouping} and {@link ThreadGrouping}s
*/
public ObservableObjectValue<CombinedGrouping> getGrouping()
{
return grouping;
}
// UI Helper Methods
/**
* Refreshes the view. The view should be updated based on the current states of the data in the subclass, and the
* {@link FilterSpecification} and {@link CombinedGrouping}s as currently selected at the
* {@link AbstractViewController} level.
*/
protected abstract void refresh();
/**
* Initialize the {@link TableView} or {@link TreeTableView} which contains the View data, if applicable.
* <p>
* Care should be taken in the subclassed to call this at the right time, because some contextual information may be
* needed. The {@link ApplicationContext} must be set for the I18N to work, and in the case of Diff column headers,
* the {@link ProfileContext}s for the profiles being compared should also be known. In the current implementation
* therefore, the method is called in the {@link AbstractProfileViewController#setProfileContext(ProfileContext)}
* and {@link AbstractProfileDiffViewController#setProfileContexts(ProfileContext, ProfileContext)} methods.
*/
protected abstract void initializeTable();
/**
* Sets the contents of the column header. This method is abstract because different implementing controllers have
* different requirements. E.g. the Diff views need to include indications of which of the profiles being compared
* the column data is for.
* <p>
* If null is passed as {@link ProfileContext}, this indicates to the subclass that the data for the column is a
* comparison between both profiles from a {@link DiffEntry}.
* <p>
*
* @param <C> the type of the column
* @param column the column for which the header contents should be set
* @param title the display title for the column
* @param context the {@link ProfileContext} of the profile for which the column contains data, or null for a Diff
* column which contains comparison data between both profiles in a Diff
*/
protected abstract <C> void setColumnHeader(C column, String title, ProfileContext context);
// Unfortunately the Cell- and CellValueFactories for TableColumns and TreeTableColumns are not compatible. As a
// result, we get full code duplication. All the following UI helper methods are provided once for TableColumns, and
// once for TreeTableColumns. Thusly ye cookie crumbleth.
// UI Helper Methods : Table Column Configuration
/**
* Configures a {@link TableColumn} containing percentages calculated for a single profile.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param profileContext the {@link ProfileContext} for the profile whose data is shown
* @param title the column title
*/
protected <U> void cfgPctCol(TableColumn<U, Number> column, String propertyName,
ProfileContext profileContext, String title)
{
column.setCellValueFactory(new PropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new PercentageTableCell<>(null));
setColumnHeader(column, title, profileContext);
}
/**
* Configures a {@link TableColumn} containing the percentage difference comparing two profiles.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param title the column title
*/
protected <U> void cfgPctDiffCol(TableColumn<U, Number> column, String propertyName,
String title)
{
column.setCellValueFactory(new PropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new PercentageTableCell<>(doubleDiffStyler));
setColumnHeader(column, title, null);
}
/**
* Configures a {@link TableColumn} containing numbers calculated for a single profile.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param profileContext the {@link ProfileContext} for the profile whose data is shown
* @param title the column title
*/
protected <U> void cfgNrCol(TableColumn<U, Number> column, String propertyName,
ProfileContext profileContext, String title)
{
column.setCellValueFactory(new PropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new CountTableCell<>(null));
setColumnHeader(column, title, profileContext);
}
/**
* Configures a {@link TableColumn} containing the number difference comparing two profiles.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param title the column title
*/
protected <U> void cfgNrDiffCol(TableColumn<U, Number> column, String propertyName,
String title)
{
column.setCellValueFactory(new PropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new CountTableCell<>(intDiffStyler));
setColumnHeader(column, title, null);
}
/**
* Configures a {@link TableColumn} containing durations calculated for a single profile.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param profileContext the {@link ProfileContext} for the profile whose data is shown
* @param title the column title
*/
protected <U> void cfgTimeCol(TableColumn<U, Number> column, String propertyName,
ProfileContext profileContext, String title)
{
column.setCellValueFactory(new PropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new TimeTableCell<>(null));
setColumnHeader(column, title, profileContext);
}
/**
* Configures a {@link TableColumn} containing the duration difference comparing two profiles.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param title the column title
*/
protected <U> void cfgTimeDiffCol(TableColumn<U, Number> column, String propertyName,
String title)
{
column.setCellValueFactory(new PropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new TimeTableCell<>(longDiffStyler));
setColumnHeader(column, title, null);
}
// UI Helper Methods : TreeTable Column Configuration
/**
* Configures a {@link TreeTableColumn} containing percentages calculated for a single profile.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param profileContext the {@link ProfileContext} for the profile whose data is shown
* @param title the column title
*/
protected <U> void cfgPctCol(TreeTableColumn<U, Number> column, String propertyName,
ProfileContext profileContext, String title)
{
column.setCellValueFactory(new TreeItemPropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new PercentageTreeTableCell<>(null));
setColumnHeader(column, title, profileContext);
}
/**
* Configures a {@link TreeTableColumn} containing the percentage difference comparing two profiles.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param title the column title
*/
protected <U> void cfgPctDiffCol(TreeTableColumn<U, Number> column, String propertyName,
String title)
{
column.setCellValueFactory(new TreeItemPropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new PercentageTreeTableCell<>(doubleDiffStyler));
setColumnHeader(column, title, null);
}
/**
* Configures a {@link TreeTableColumn} containing numbers calculated for a single profile.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param profileContext the {@link ProfileContext} for the profile whose data is shown
* @param title the column title
*/
protected <U> void cfgNrCol(TreeTableColumn<U, Number> column, String propertyName,
ProfileContext profileContext, String title)
{
column.setCellValueFactory(new TreeItemPropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new CountTreeTableCell<>(null));
setColumnHeader(column, title, profileContext);
}
/**
* Configures a {@link TreeTableColumn} containing the number difference comparing two profiles.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param title the column title
*/
protected <U> void cfgNrDiffCol(TreeTableColumn<U, Number> column, String propertyName,
String title)
{
column.setCellValueFactory(new TreeItemPropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new CountTreeTableCell<>(intDiffStyler));
setColumnHeader(column, title, null);
}
/**
* Configures a {@link TreeTableColumn} containing durations calculated for a single profile.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param profileContext the {@link ProfileContext} for the profile whose data is shown
* @param title the column title
*/
protected <U> void cfgTimeCol(TreeTableColumn<U, Number> column, String propertyName,
ProfileContext profileContext, String title)
{
column.setCellValueFactory(new TreeItemPropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new TimeTreeTableCell<>(null));
setColumnHeader(column, title, profileContext);
}
/**
* Configures a {@link TableColumn} containing the duration difference comparing two profiles.
* <p>
*
* @param <U> the type of the items in the {@link TableView} containing the {@link TableColumn}
* @param column the column being configured
* @param propertyName the name of the property containing the value for this column
* @param title the column title
*/
protected <U> void cfgTimeDiffCol(TreeTableColumn<U, Number> column, String propertyName,
String title)
{
column.setCellValueFactory(new TreeItemPropertyValueFactory<>(propertyName));
column.setCellFactory(col -> new TimeTreeTableCell<>(longDiffStyler));
setColumnHeader(column, title, null);
}
// Filter-related methods
/**
* Returns the current {@link FilterSpecification}.
* <p>
*
* @return the current {@link FilterSpecification}
*/
protected FilterSpecification<T> getFilterSpecification()
{
return filterSpec.get();
}
/**
* Initializes the filters.
* <p>
* The {@link ApplicationContext} parameter is used to explicitly point out the dependency on the presence of the
* context, to anybody who wants to modify {@link AbstractViewController}.
* <p>
*
* @param applicationContext the {@link ApplicationContext}
*/
private void initializeFilters(ApplicationContext applicationContext)
{
// Prepare the filter selection Dialog
filterController.setApplicationContext(applicationContext);
filterController.setItemType(type);
// When the FilterSpecification is updated, refresh the view and update the button icon.
filterSpec.addListener((property, oldValue, newValue) ->
{
filterButton.setGraphic(iconFor(newValue));
refresh();
});
// Show the filter selection Dialog when pushing the Button.
filterButton.setOnAction(event -> filterSpec.set(filterController.showAndWait().get()));
// Apply the QuickFilter when the quickFilter button is Pressed.
quickFilterButton.setOnAction(event -> applyQuickFilter());
// Apply the QuickFilter when the user presses RETURN in the quickFilter text area.
quickFilterText.setOnKeyPressed(event ->
{
if (event.getCode() == ENTER)
{
applyQuickFilter();
}
});
}
/**
* Select the appropriate icon for the state of the specified {@link FilterSpecification}.
* <p>
*
* @param spec the {@link FilterSpecification}
* @return an {@link ImageView} for the appropriate icon
*/
private ImageView iconFor(FilterSpecification<T> spec)
{
return spec == null || !spec.isFiltering() ? viewFor(FUNNEL_16) : viewFor(FUNNEL_ACTIVE_16);
}
/**
* Apply the quickFilter by updating the {@link FilterSpecification} (which will trigger the {@link ChangeListener}
* which will refresh the view).
*/
private void applyQuickFilter()
{
String input = quickFilterText.getText();
filterSpec.get().setQuickFilter(input == null || input.isEmpty() ? null : input);
refresh();
}
/**
* Shows or hides the selected {@link Node}s. Used to show or hide the grouping {@link ChoiceBox}es.
* <p>
*
* @param visible a boolean indicating whether the {@link Node}s should be visible
* @param nodes the {@link Node}s to be made (in)visible
*/
private void setVisibility(boolean visible, Node... nodes)
{
for (Node node : nodes)
{
node.setVisible(visible);
node.setManaged(visible);
}
}
/**
* Configures the grouping controls, ensuring a new {@link CombinedGrouping} is set whenever either a new
* {@link ThreadGrouping} or {@link FrameGrouping} is selected.
* <p>
* Also sets the initial selection to the first grouping supplied by the
* {@link #setAllowedFrameGroupings(FrameGrouping...)} and {@link #setAllowedThreadGroupings(ThreadGrouping...)}
* methods.
*/
private void bindGroupings()
{
if (threadGrouping == null
|| frameGrouping == null
|| threadGrouping.getItems().size() == 0
|| frameGrouping.getItems().size() == 0)
{
return;
}
threadGrouping.getSelectionModel().selectedItemProperty().addListener(
(property, oldValue, newValue) ->
{
if (newValue != null)
{
FrameGrouping other = frameGrouping.getSelectionModel().getSelectedItem();
if (other != null)
{
grouping.set(combine(newValue, other));
}
}
});
frameGrouping.getSelectionModel().selectedItemProperty().addListener(
(property, oldValue, newValue) ->
{
if (newValue != null)
{
ThreadGrouping other = threadGrouping.getSelectionModel().getSelectedItem();
if (other != null)
{
grouping.set(combine(other, newValue));
}
}
});
threadGrouping.getSelectionModel().select(0);
frameGrouping.getSelectionModel().select(0);
}
}