package com.insightfullogic.honest_profiler.ports.javafx.controller;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.addProfileNr;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.createColoredLabelContainer;
import static javafx.beans.binding.Bindings.createObjectBinding;
import static javafx.geometry.Pos.CENTER;
import java.util.function.Function;
import com.insightfullogic.honest_profiler.core.aggregation.grouping.CombinedGrouping;
import com.insightfullogic.honest_profiler.core.aggregation.result.ItemType;
import com.insightfullogic.honest_profiler.core.aggregation.result.diff.AbstractDiff;
import com.insightfullogic.honest_profiler.ports.javafx.model.ProfileContext;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableObjectValue;
import javafx.scene.Node;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
/**
* Superclass for all Diff View Controllers in the application which provide a view on a comparison between two
* "targets", data structures of type T which are each stored in their respective target {@link ObjectProperty}s.The
* targets are extracted from source {@link ObservableObjectValue}s using an extractor function.
* <p>
* This superclass also serves as a repository for the {@link ProfileContext}s associated to the profiles being compared
* in the "Diff View".
* <p>
* * This superclass ensures that subclass refresh() implementations are called when the source
* {@link ObservableObjectValue}s or the {@link CombinedGrouping} from the {@link AbstractViewController} superclass are
* updated. The extractor function is then used to extract new targets from the sources, using the new
* {@link CombinedGrouping} if available.
* <p>
* By binding or unbinding the local targets, it is possible to start and stop all tracking of changes to the targets in
* the UI. This has been provided to make it possible to stop executing refresh() and other UI updates when the view
* associated to the controller is hidden.
* <p>
* The superclass also provides some common UI helper methods for column configuration.
* <p>
* @see AbstractDiff class javadoc for an explanation of the "Base" and "New" terminology
* <p>
* @param <T> the data type of the targets
* @param <U> the type of the items contained in the View
*/
public abstract class AbstractProfileDiffViewController<T, U> extends AbstractViewController<U>
{
// Instance Properties
private ProfileContext baseContext;
private ProfileContext newContext;
private ObjectProperty<T> baseTarget;
private ObjectProperty<T> newTarget;
private ObjectBinding<T> baseSourceBinding;
private ObjectBinding<T> newSourceBinding;
// FXML Implementation
/**
* Initialize method for subclasses which sets the basic properties needed by this superclass. This method must be
* called by such subclasses in their FXML initialize().
* <p>
* @param type the {@link ItemType} specifying the type of items shown in the View
*/
@Override
protected void initialize(ItemType type)
{
super.initialize(type);
baseTarget = new SimpleObjectProperty<>();
newTarget = new SimpleObjectProperty<>();
baseTarget.addListener((property, oldValue, newValue) -> refresh());
newTarget.addListener((property, oldValue, newValue) -> refresh());
}
// Instance Accessors
/**
* Returns the {@link ProfileContext} for the baseline target. The name has been shortened to unclutter code in
* subclasses.
* <p>
* @return the {@link ProfileContext} encapsulating the baseline target.
*/
protected ProfileContext baseCtx()
{
return baseContext;
}
/**
* Returns the {@link ProfileContext} for the "new" target which will be compared against the baseline. The name has
* been shortened to unclutter code in subclasses.
* <p>
* @return the {@link ProfileContext} encapsulating the target being compared against the baseline.
*/
protected ProfileContext newCtx()
{
return newContext;
}
/**
* Returns the current baseline target instance.
* <p>
* @return the current baseline target instance
*/
protected T getBaseTarget()
{
return baseTarget.get();
}
/**
* Returns the current "new" target instance.
* <p>
* @return the current "new" target instance
*/
protected T getNewTarget()
{
return newTarget.get();
}
/**
* Sets the {@link ProfileContext}s encapsulating the baseline target and the target being compared against it.
* <p>
* @param baseContext the {@link ProfileContext}s encapsulating the baseline target
* @param newContext the {@link ProfileContext}s encapsulating the target being compared
*/
public void setProfileContexts(ProfileContext baseContext, ProfileContext newContext)
{
this.baseContext = baseContext;
this.newContext = newContext;
// Called here because for the Diff column headers, the profile contexts are needed to display the profile
// number label.
initializeTable();
}
// Source-Target Binding
/**
* Bind the supplied extractor function which extracts the target data structure T from the source to the source
* {@link ObservableObjectValue}s, and optionally to the {@link CombinedGrouping} {@link ObservableObjectValue} from
* the {@link AbstractViewController} superclass if present.
* <p>
* @param baseSource the {@link ObservableObjectValue} encapsulating the source from which the Base target data
* structure can be extracted
* @param newSource the {@link ObservableObjectValue} encapsulating the source from which the New target data
* structure can be extracted
* @param targetExtractor a function which extracts the target from the source Object
*/
public void bind(ObjectProperty<? extends Object> baseSource,
ObjectProperty<? extends Object> newSource, Function<Object, T> targetExtractor)
{
// The createObjectBinding() dependency varargs parameter specifies a number of Observables. If the value of any
// of those changes, the Binding is triggered and the specified function is executed. This is IMHO not so
// clearly documented in the createObjectBinding() javadoc.
// The View does not support CombinedGrouping.
if (getGrouping() == null)
{
baseSourceBinding = createObjectBinding(
() -> targetExtractor.apply(baseSource.get()),
baseSource);
newSourceBinding = createObjectBinding(
() -> targetExtractor.apply(newSource.get()),
newSource);
}
// The View does supports CombinedGrouping.
else
{
baseSourceBinding = createObjectBinding(
() -> targetExtractor.apply(baseSource.get()),
baseSource,
getGrouping());
newSourceBinding = createObjectBinding(
() -> targetExtractor.apply(newSource.get()),
newSource,
getGrouping());
}
}
// Activation Methods
/**
* Activate or deactivate the current view. When activated, the view tracks changes in the target.
* <p>
* @param active a boolean indicating whether to activate or deactivate the view.
*/
public void setActive(boolean active)
{
if (active)
{
// Binds the local target ObjectProperties to the sourceBindings created with the bind() method. The net
// effect is that the controller will start tracking changes to the target instances.
baseTarget.bind(baseSourceBinding);
newTarget.bind(newSourceBinding);
}
else
{
// Unbinds the local target ObjectProperties. The controller no longer tracks changes to the source
// ObservableObjectvalues.
baseTarget.unbind();
newTarget.unbind();
}
}
// AbstractViewController Implementation
@Override
protected <C> void setColumnHeader(C column, String title, ProfileContext profileContext)
{
HBox header = createColoredLabelContainer(CENTER);
if (profileContext != null)
{
addProfileNr(header, profileContext);
}
header.getChildren().add(new Text(title));
// Somehow it's hard to get a TableColumn to resize properly.
// Therefore, we calculate a fair width ourselves.
double width = calculateWidth(header);
reconfigure((TableColumnBase<?, ?>)column, null, header, width, width + 5);
}
/**
* Set various {@link TableColumnBase} properties.
* <p>
* @param column the {@link TreeTableColumn} to be reconfigured
* @param text the text to be displayed in the column header
* @param graphic the graphic to be displayed in the column header
* @param minWidth the minimum width of the column
* @param prefWidth the preferred width of the coumn
*/
private void reconfigure(TableColumnBase<?, ?> column, String text, Node graphic,
double minWidth, double prefWidth)
{
column.setText(text);
column.setGraphic(graphic);
column.setMinWidth(minWidth);
column.setPrefWidth(prefWidth);
}
/**
* Calculate a column width for a column with the specified box as header graphic.
* <p>
* @param box the header graphic for the column
* @return the calculated width
*/
private double calculateWidth(HBox box)
{
double width = 0;
for (Node node : box.getChildren())
{
width += node.getBoundsInLocal().getWidth();
}
width += box.getSpacing() * (box.getChildren().size() - 1);
width += box.getPadding().getLeft() + box.getPadding().getRight();
return width;
}
}