/**
* erlyberly, erlang trace debugger
* Copyright (C) 2016 Andy Till
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package erlyberly;
import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.ResourceBundle;
import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangList;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangTuple;
import de.jensd.fx.fontawesome.AwesomeIcon;
import floatyfield.FloatyFieldView;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;
import javafx.scene.control.SplitPane.Divider;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import ui.FAIcon;
import ui.TabPaneDetacher;
public class DbgView implements Initializable {
private static final String MODULES_TREE_PREF_WIDTH_CONFIG_KEY = "modulesTreePrefWidth";
private static final double MODULES_TREE_PREF_WIDTH_CONFIG_KEY_DEFAULT = 300;
private static final String ICON_STYLE = "-fx-font-family: FontAwesome; -fx-font-size: 1em;";
private final DbgController dbgController = new DbgController();
private final ObservableList<TreeItem<ModFunc>> treeModules = FXCollections.observableArrayList();
private final SortedList<TreeItem<ModFunc>> sortedTreeModules = new SortedList<TreeItem<ModFunc>>(treeModules);
private final FilteredList<TreeItem<ModFunc>> filteredTreeModules = new FilteredList<TreeItem<ModFunc>>(sortedTreeModules);
/**
* A list of all the filtered lists for functions, so a predicate can be set on them. Binding
* the predicate property does not seem to work.
*/
private final HashMap<ModFunc, FilteredList<TreeItem<ModFunc>>> functionLists = new HashMap<>();
@FXML
private TreeView<ModFunc> modulesTree;
@FXML
private VBox modulesBox;
@FXML
private Label noTracesLabel;
@FXML
private SplitPane dbgSplitPane;
@FXML
private HBox traceLogSearchBox;
private double functionsDivPosition;
private ModFuncContextMenu modFuncContextMenu;
@Override
public void initialize(URL url, ResourceBundle r) {
modFuncContextMenu = new ModFuncContextMenu(dbgController);
modFuncContextMenu.setModuleTraceMenuText("Trace All Functions in Module");
modFuncContextMenu.rootProperty().bind(modulesTree.rootProperty());
modulesTree
.getSelectionModel()
.selectedItemProperty()
.addListener((o, old, newItem) -> {
modFuncContextMenu.selectedTreeItemProperty().set(newItem);
if(newItem != null)
modFuncContextMenu.selectedItemProperty().set(newItem.getValue());
});
sortedTreeModules.setComparator(treeItemModFuncComparator());
SplitPane.setResizableWithParent(modulesBox, Boolean.FALSE);
ErlyBerly.nodeAPI().connectedProperty().addListener(this::onConnected);
modulesTree.setCellFactory(new ModFuncTreeCellFactory(dbgController));
modulesTree.setContextMenu(modFuncContextMenu);
addModulesFloatySearchControl();
dbgController.initialize(url, r);
dbgController.setModuleLoadedCallback((tuple) -> {
createModuleTreeItem(tuple);
});
tabPane = new TabPane();
TabPaneDetacher.create()
.stylesheets("/floatyfield/floaty-field.css", "/erlyberly/erlyberly.css")
.makeTabsDetachable(tabPane);
Tab traceViewTab;
traceViewTab = new Tab("Traces");
traceViewTab.setContent(new DbgTraceView(dbgController));
traceViewTab.setClosable(false);
getTabPane().getTabs().add(traceViewTab);
dbgSplitPane.getItems().add(getTabPane());
}
private FxmlLoadable addModulesFloatySearchControl() {
FxmlLoadable loader = new FxmlLoadable("/floatyfield/floaty-field.fxml");
loader.load();
FloatyFieldView ffView;
ffView = (FloatyFieldView) loader.controller;
ffView.promptTextProperty().set("Filter functions i.e. gen_s:call or #t for all traces");
loader.fxmlNode.setStyle("-fx-padding: 5 5 0 5;");
HBox.setHgrow(loader.fxmlNode, Priority.ALWAYS);
modulesBox.getChildren().add(0, loader.fxmlNode);
filterTextProperty = ffView.textProperty();
filterTextProperty.addListener(this::onFunctionSearchChange);
TextField filterTextView;
filterTextView = floatyFieldTextField(loader);
Platform.runLater(() -> {
FilterFocusManager.addFilter(filterTextView, 1);
});
return loader;
}
/**
* Flag for when the shortcut for applying traces to all visisble functions
* is pressed down, used for only executing it once per press, not once per
* event which can be many.
*/
static boolean toggleAllTracesDown = false;
private StringProperty filterTextProperty;
private TabPane tabPane;
private TextField floatyFieldTextField(FxmlLoadable loader) {
// FIXME floaty field should allow access to the text field
return (TextField) loader.fxmlNode.getChildrenUnmodifiable().get(1);
}
public void onRefreshModules(ActionEvent e) {
treeModules.clear();
refreshModules();
}
public void onFunctionSearchChange(Observable o, String oldValue, String search) {
if(isSpecialTraceFilter(search))
filterForTracedFunctions();
else
filterForFunctionTextMatch(search);
}
private boolean isSpecialTraceFilter(String search) {
return "#t".equals(search.trim());
}
private void filterForFunctionTextMatch(String search) {
String[] split = search.split(":");
if(split.length == 0)
return;
final String moduleName = split[0];
final String funcName = (split.length > 1) ? split[1] : "";
if(search.contains(":")) {
for (TreeItem<ModFunc> treeItem : filteredTreeModules) {
treeItem.setExpanded(true);
}
}
for (FilteredList<TreeItem<ModFunc>> funcItemList : functionLists.values()) {
funcItemList.setPredicate((t) -> { return isMatchingModFunc(funcName, t); });
}
filteredTreeModules.setPredicate((t) -> { return isMatchingModFunc(moduleName, t) && !t.getChildren().isEmpty(); });
}
private void filterForTracedFunctions() {
for (FilteredList<TreeItem<ModFunc>> funcItemList : functionLists.values()) {
funcItemList.setPredicate((t) -> { return dbgController.isTraced(t.getValue()); });
}
filteredTreeModules.setPredicate((t) -> { return !t.getChildren().isEmpty(); });
}
private Comparator<TreeItem<ModFunc>> treeItemModFuncComparator() {
return new Comparator<TreeItem<ModFunc>>() {
@Override
public int compare(TreeItem<ModFunc> o1, TreeItem<ModFunc> o2) {
return o1.getValue().compareTo(o2.getValue());
}};
}
private boolean isMatchingModFunc(String searchText, TreeItem<ModFunc> t) {
if(searchText.isEmpty())
return true;
return t.getValue().toString().contains(searchText);
}
private void onConnected(Observable o) {
boolean connected = ErlyBerly.nodeAPI().connectedProperty().get();
// disable buttons when not connected
/*seqTraceMenuItem.setDisable(!connected);*/
if(connected) {
refreshModules();
dbgController.reapplyTraces();
} else {
// Don't clear the Traces, keep it, an re-apply once connected again.
treeModules.clear();
}
}
private void refreshModules() {
try {
modulesTree.setShowRoot(false);
dbgController.requestModFuncs(this::buildObjectTreeRoot);
}
catch (Exception e) {
throw new RuntimeException("failed to build module/function tree", e);
}
}
private void buildObjectTreeRoot(OtpErlangList requestFunctions) {
for (OtpErlangObject e : requestFunctions) {
OtpErlangTuple tuple = (OtpErlangTuple) e;
createModuleTreeItem(tuple);
}
TreeItem<ModFunc> root;
root = new TreeItem<ModFunc>();
root.setExpanded(true);
Bindings.bindContentBidirectional(root.getChildren(), filteredTreeModules);
modulesTree.setRoot(root);
// set predicates on the function tree items so that they filter correctly
filterForFunctionTextMatch(filterTextProperty.get());
}
private void createModuleTreeItem(OtpErlangTuple tuple) {
boolean isExported;
OtpErlangAtom moduleNameAtom = (OtpErlangAtom) tuple.elementAt(0);
OtpErlangList exportedFuncs = (OtpErlangList) tuple.elementAt(1);
OtpErlangList localFuncs = (OtpErlangList) tuple.elementAt(2);
TreeItem<ModFunc> moduleItem;
ModFunc module = ModFunc.toModule(moduleNameAtom);
moduleItem = new TreeItem<ModFunc>(module);
moduleItem.setGraphic(treeIcon(AwesomeIcon.CUBE));
ObservableList<TreeItem<ModFunc>> modFuncs = FXCollections.observableArrayList();
SortedList<TreeItem<ModFunc>> sortedFuncs = new SortedList<TreeItem<ModFunc>>(modFuncs);
FilteredList<TreeItem<ModFunc>> filteredFuncs = new FilteredList<TreeItem<ModFunc>>(sortedFuncs);
sortedFuncs.setComparator(treeItemModFuncComparator());
isExported = true;
addTreeItems(toModFuncs(moduleNameAtom, exportedFuncs, isExported), modFuncs);
isExported = false;
addTreeItems(toModFuncs(moduleNameAtom, localFuncs, isExported), modFuncs);
functionLists.put(module, filteredFuncs);
Bindings.bindContentBidirectional(moduleItem.getChildren(), filteredFuncs);
ArrayList<TreeItem<ModFunc>> treeModulesCopy = new ArrayList<>(treeModules);
for (TreeItem<ModFunc> treeItem : treeModulesCopy) {
if(treeItem.getValue().equals(module)) {
treeModules.remove(treeItem);
}
}
treeModules.add(moduleItem);
}
private void addTreeItems(List<ModFunc> modFuncs, ObservableList<TreeItem<ModFunc>> modFuncTreeItems) {
for (ModFunc modFunc : modFuncs) {
if(!modFunc.isSynthetic()) {
TreeItem<ModFunc> item = newFuncTreeItem(modFunc);
modFuncTreeItems.add(item);
}
}
}
private TreeItem<ModFunc> newFuncTreeItem(ModFunc modFunc) {
return new TreeItem<ModFunc>(modFunc);
}
private FAIcon treeIcon(AwesomeIcon treeIcon) {
return FAIcon.create().icon(treeIcon).style(ICON_STYLE);
}
private ArrayList<ModFunc> toModFuncs(OtpErlangAtom moduleNameAtom, OtpErlangList exportedFuncs, boolean isExported) {
ArrayList<ModFunc> mfs = new ArrayList<>();
for (OtpErlangObject exported : exportedFuncs) {
ModFunc modFunc = ModFunc.toFunc(moduleNameAtom, exported, isExported);
mfs.add(modFunc);
}
return mfs;
}
public void setFunctionsVisibility(Boolean hidden) {
if(!hidden) {
dbgSplitPane.getItems().add(0, modulesBox);
Divider div = dbgSplitPane.getDividers().get(0);
div.setPosition(functionsDivPosition);
}
else {
Divider div = dbgSplitPane.getDividers().get(0);
functionsDivPosition = div.getPosition();
div.setPosition(0d);
dbgSplitPane.getItems().remove(0);
}
}
public TabPane getTabPane() {
return tabPane;
}
public void sizeSplitPanes() {
assert dbgSplitPane.getScene() != null;
assert dbgSplitPane.getScene().getWidth() > 0.0d;
try {
double percent = (configuredModulesWidth() / dbgSplitPane.getScene().getWidth());
// the split pane divider position can only be set as a percentage of the split pane
dbgSplitPane.setDividerPosition(0, percent);
}
catch (NumberFormatException e) {
e.printStackTrace();
}
// whenever the width of the pane changes, write it to configuration
// this is buffered so rapid writes do not cause rapid writes to disk
modulesBox.widthProperty().addListener((o, ov, nv) -> {
PrefBind.set(MODULES_TREE_PREF_WIDTH_CONFIG_KEY, nv);
});
}
private double configuredModulesWidth() {
return PrefBind.getOrDefaultDouble(MODULES_TREE_PREF_WIDTH_CONFIG_KEY, MODULES_TREE_PREF_WIDTH_CONFIG_KEY_DEFAULT);
}
}