/**
* 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.io.IOException;
import java.net.URL;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import com.ericsson.otp.erlang.OtpErlangException;
import com.ericsson.otp.erlang.OtpErlangObject;
import de.jensd.fx.fontawesome.AwesomeIcon;
import erlyberly.node.AppProcs;
import erlyberly.node.OtpUtil;
import floatyfield.FloatyFieldView;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.PieChart.Data;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Separator;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToolBar;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import ui.FAIcon;
public class TopBarView implements Initializable {
private static final KeyCodeCombination TOGGLE_HIDE_PROCESSES_SHORTCUT = new KeyCodeCombination(KeyCode.P, KeyCombination.SHORTCUT_DOWN);
private static final KeyCodeCombination TOGGLE_HIDE_MODULES_SHORTCUT = new KeyCodeCombination(KeyCode.M, KeyCombination.SHORTCUT_DOWN);
private static final KeyCodeCombination REFRESH_MODULES_SHORTCUT = new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN);
private final SimpleIntegerProperty unreadCrashReportsProperty = new SimpleIntegerProperty(0);
private final SimpleBooleanProperty isXrefAnalysing = new SimpleBooleanProperty();
@FXML
private ToggleButton hideProcessesButton;
@FXML
private ToggleButton hideFunctionsButton;
@FXML
private Button refreshModulesButton;
@FXML
private Button erlangMemoryButton;
private MenuButton crashReportsButton = new MenuButton("Crash Reports");
@FXML
private Button xrefAnalysisButton;
@FXML
private Button disconnectButton;
@FXML
private Button prefButton;
@FXML
private Button suspendButton;
@FXML
private ToolBar topBox;
private EventHandler<ActionEvent> refreshModulesAction;
@Override
public void initialize(URL url, ResourceBundle r) {
topBox.getItems().add(crashReportsButton);
// TODO: Should we hide these buttons, when disconnected ?
hideProcessesButton.setGraphic(FAIcon.create().icon(AwesomeIcon.RANDOM));
hideProcessesButton.setContentDisplay(ContentDisplay.TOP);
hideProcessesButton.setGraphicTextGap(0d);
hideProcessesButton.setTooltip(new Tooltip("Show/Hide the Processes (ctrl+p)"));
// TODO: Should we hide these buttons, when disconnected ?
hideFunctionsButton.setGraphic(FAIcon.create().icon(AwesomeIcon.CUBE));
hideFunctionsButton.setContentDisplay(ContentDisplay.TOP);
hideFunctionsButton.setGraphicTextGap(0d);
hideFunctionsButton.setTooltip(new Tooltip("Show/Hide the Modules (ctrl+m)"));
refreshModulesButton.setGraphic(FAIcon.create().icon(AwesomeIcon.ROTATE_LEFT));
refreshModulesButton.setContentDisplay(ContentDisplay.TOP);
refreshModulesButton.setGraphicTextGap(0d);
refreshModulesButton.setTooltip(new Tooltip("Refresh Modules and Functions to show new, hot-loaded code (ctrl+r)"));
refreshModulesButton.disableProperty().bind(ErlyBerly.nodeAPI().connectedProperty().not());
erlangMemoryButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART));
erlangMemoryButton.setContentDisplay(ContentDisplay.TOP);
erlangMemoryButton.setGraphicTextGap(0d);
erlangMemoryButton.setTooltip(new Tooltip("Refresh Modules and Functions to show new, hot-loaded code (ctrl+r)"));
erlangMemoryButton.disableProperty().bind(ErlyBerly.nodeAPI().connectedProperty().not());
// TODO: maybe make this button available, sometimes, there are crashes causing the node to go down,
// and then it's disabled when disconnected....( i would like to see the crashes ) :)
crashReportsButton.setGraphic(crashReportsGraphic());
crashReportsButton.setContentDisplay(ContentDisplay.LEFT);
crashReportsButton.setGraphicTextGap(0d);
crashReportsButton.setTooltip(new Tooltip("View crash reports received from the connected node."));
// disable the button if we're not connected or there are no crash report menu items
crashReportsButton.disableProperty().bind(
ErlyBerly.nodeAPI().connectedProperty().not()
.or(Bindings.size(crashReportsButton.getItems()).isEqualTo(2)));
crashReportsButton.setStyle("-fx-font-size: 10; -fx-padding: 5 5 5 5;");
crashReportsButton.getItems().addAll(removeCrashReportsMenuItem(), new SeparatorMenuItem());
ErlyBerly.nodeAPI().getCrashReports()
.addListener((ListChangeListener.Change<? extends OtpErlangObject> e) -> {
while(e.next()) {
for (OtpErlangObject obj : e.getAddedSubList()) {
CrashReport crashReport = new CrashReport(obj);
MenuItem menuItem;
menuItem = new MenuItem();
menuItem.setGraphic(new CrashReportGraphic(crashReport));
menuItem.setOnAction((action) -> {
unreadCrashReportsProperty.set(0);
ErlyBerly.showPane("Crash Report", ErlyBerly.wrapInPane(crashReportView(crashReport)));
});
crashReportsButton.getItems().add(menuItem);
}
}
});
xrefAnalysisButton.setGraphic(xrefAnalysisGraphic());
xrefAnalysisButton.setContentDisplay(ContentDisplay.TOP);
xrefAnalysisButton.setGraphicTextGap(0d);
xrefAnalysisButton.setTooltip(new Tooltip("Start xref analysis. This may take a while, an ok is displayed when complete."));
xrefAnalysisButton.disableProperty().bind(
ErlyBerly.nodeAPI().connectedProperty().not().or(isXrefAnalysing).or(ErlyBerly.nodeAPI().xrefStartedProperty()));
xrefAnalysisButton.setOnAction((e) -> {
try {
ErlyBerly.nodeAPI().asyncEnsureXRefStarted();
isXrefAnalysing.set(true);
} catch (Exception e1) {
e1.printStackTrace();
}
});
disconnectButton.setGraphic(FAIcon.create().icon(AwesomeIcon.EJECT));
disconnectButton.setContentDisplay(ContentDisplay.TOP);
disconnectButton.setGraphicTextGap(0d);
disconnectButton.setTooltip(new Tooltip("Disconnect"));
disconnectButton.disableProperty().bind(ErlyBerly.nodeAPI().connectedProperty().not());
disconnectButton.setOnAction((e) -> {
try {
disconnect();
} catch (Exception e1) {
e1.printStackTrace();
}
});
prefButton.setGraphic(FAIcon.create().icon(AwesomeIcon.GEARS));
prefButton.setContentDisplay(ContentDisplay.TOP);
prefButton.setGraphicTextGap(0d);
prefButton.setTooltip(new Tooltip("Preferences"));
prefButton.disableProperty().bind(ErlyBerly.nodeAPI().connectedProperty().not());
prefButton.setOnAction((e) -> { displayPreferencesPane(); });
suspendButton.setContentDisplay(ContentDisplay.TOP);
suspendButton.setGraphicTextGap(0d);
suspendButton.setTooltip(new Tooltip("Toggle Trace Suspension"));
suspendButton.disableProperty().bind(ErlyBerly.nodeAPI().connectedProperty().not());
suspendButton.setOnAction((e) -> { suspendTraces(); });
// set the default text and icon
onSuspendedStateChanged(false);
// listen to when tracing is suspend or not, and update the button text and icon
ErlyBerly.nodeAPI().suspendedProperty().addListener((o,oldv,suspended) -> {
onSuspendedStateChanged(suspended);
});
hideProcsProperty().addListener((Observable o) -> { toggleHideProcs(); });
hideFunctionsProperty().addListener((Observable o) -> { toggleHideFuncs(); });
erlangMemoryButton.setOnAction((e) -> { showErlangMemory(); });
FxmlLoadable loader = processCountStat();
topBox.getItems().add(new Separator(Orientation.VERTICAL));
topBox.getItems().add(loader.fxmlNode);
// let's store the ui preferences, as the end user changes them...
PrefBind.bindBoolean("hideProcesses", hideProcessesButton.selectedProperty());
PrefBind.bindBoolean("hideModules", hideFunctionsButton.selectedProperty());
boolean hideProcs = PrefBind.getOrDefaultBoolean("hideProcesses", false);
boolean hideMods = PrefBind.getOrDefaultBoolean("hideModules", false);
if(hideProcs){
// click the hide button manually.
hideProcessesButton.setSelected(true);
}
if(hideMods){
// click the hide button manually.
hideFunctionsButton.setSelected(true);
}
toggleHideProcs();
toggleHideFuncs();
ErlyBerly.nodeAPI()
.getCrashReports()
.addListener(this::traceLogsChanged);
ErlyBerly.nodeAPI()
.xrefStartedProperty()
.addListener((e, oldv, newv) -> { if(newv) isXrefAnalysing.set(false); });
}
private void onSuspendedStateChanged(Boolean suspended) {
if(suspended) {
suspendButton.setText("Unsuspend");
suspendButton.getStyleClass().add("button-suspended");
}
else {
suspendButton.setText("Suspend");
suspendButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PAUSE));
suspendButton.getStyleClass().remove("button-suspended");
}
}
private void suspendTraces() {
try {
ErlyBerly.nodeAPI().toggleSuspended();
} catch (OtpErlangException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private MenuItem removeCrashReportsMenuItem() {
MenuItem menuItem;
menuItem = new MenuItem("Remove All Reports");
menuItem.setOnAction((e) -> {
ObservableList<MenuItem> items = crashReportsButton.getItems();
if(items.size() == 2)
return;
// the first two items are this menu item and a separator, delete
// everything after that
items.remove(2, items.size());
unreadCrashReportsProperty.set(0);
});
return menuItem;
}
public void traceLogsChanged(ListChangeListener.Change<? extends OtpErlangObject> e) {
while(e.next()) {
int size = e.getAddedSubList().size();
unreadCrashReportsProperty.set(unreadCrashReportsProperty.get() + size);
}
}
private CrashReportView crashReportView(CrashReport crashReport) {
CrashReportView crashReportView;
crashReportView = new CrashReportView();
crashReportView.setCrashReport(crashReport);
return crashReportView;
}
private Parent crashReportsGraphic() {
FAIcon icon;
icon = FAIcon.create().icon(AwesomeIcon.WARNING);
icon.setPadding(new Insets(0, 5, 0, 5));
Label reportCountLabel;
reportCountLabel = new Label("122");
reportCountLabel.setStyle("-fx-background-color:red; -fx-font-size:9; -fx-padding: 0 2 0 2; -fx-opacity:0.7");
reportCountLabel.setTextFill(Color.WHITE);
reportCountLabel.setText(unreadCrashReportsProperty.getValue().toString());
unreadCrashReportsProperty.addListener((o, oldv, newv) -> { reportCountLabel.setText(newv.toString()); });
reportCountLabel.visibleProperty().bind(unreadCrashReportsProperty.greaterThan(0));
StackPane stackPane = new StackPane(icon, reportCountLabel);
StackPane.setAlignment(reportCountLabel, Pos.TOP_RIGHT);
return stackPane;
}
private Parent xrefAnalysisGraphic() {
FAIcon icon;
icon = FAIcon.create().icon(AwesomeIcon.TH_LARGE);
icon.setPadding(new Insets(0, 5, 0, 5));
icon.visibleProperty().bind(isXrefAnalysing.not());
Label reportCountLabel;
reportCountLabel = new Label("ok");
reportCountLabel.setStyle("-fx-background-color:green; -fx-font-size:9; -fx-padding: 0 2 0 2; -fx-opacity:0.9");
reportCountLabel.setTextFill(Color.WHITE);
ProgressIndicator analysisProgressIndicator;
analysisProgressIndicator = new ProgressIndicator();
analysisProgressIndicator.visibleProperty().bind(isXrefAnalysing);
analysisProgressIndicator.setPrefSize(10d, 10d);
analysisProgressIndicator.setStyle("-fx-progress-color: black;");
reportCountLabel.visibleProperty().bind(ErlyBerly.nodeAPI().xrefStartedProperty());
StackPane stackPane = new StackPane(icon, reportCountLabel, analysisProgressIndicator);
StackPane.setAlignment(reportCountLabel, Pos.TOP_RIGHT);
return stackPane;
}
private void showErlangMemory() {
ObservableList<PieChart.Data> data = FXCollections.observableArrayList();
showPieChart(data);
ErlangMemoryThread emThread;
emThread = new ErlangMemoryThread(data);
emThread.start();
}
private void showPieChart(ObservableList<PieChart.Data> data) {
String title = "Erlang Memory";
PieChart pieChart;
pieChart = new PieChart(data);
pieChart.setTitle(title);
ErlyBerly.showPane(title, ErlyBerly.wrapInPane(pieChart));
}
/**
* these have to be run after initialisation is complete or an exception occurs
*/
public void addAccelerators() {
Platform.runLater(() -> {
accelerators().put(TOGGLE_HIDE_PROCESSES_SHORTCUT, () -> { invertSelection(hideProcessesButton); });
});
Platform.runLater(() -> {
accelerators().put(TOGGLE_HIDE_MODULES_SHORTCUT, () -> { invertSelection(hideFunctionsButton); });
});
Platform.runLater(() -> {
accelerators().put(REFRESH_MODULES_SHORTCUT, () -> {
if(refreshModulesAction != null)
refreshModulesAction.handle(null);
});
});
}
private FxmlLoadable processCountStat() {
FxmlLoadable loader = new FxmlLoadable("/floatyfield/floaty-field.fxml");
loader.load();
Parent fxmlNode;
fxmlNode = loader.fxmlNode;
fxmlNode.getStyleClass().add("floaty-label");
FloatyFieldView ffView;
ffView = (FloatyFieldView) loader.controller;
ffView.promptTextProperty().set("Processes");
ffView.textProperty().set("0");
ffView.disableProperty().set(true);
ErlyBerly.nodeAPI().appProcsProperty().addListener((o, ov, nv) -> { upateProcsStat(ffView, nv); });
return loader;
}
private void upateProcsStat(FloatyFieldView ffView, AppProcs nv) {
String dateString = nv.getDateTime().format(DateTimeFormatter.ISO_TIME);
ffView.textProperty().set(Integer.toString(nv.getProcCount()));
ffView.promptTextProperty().set("Processes @ " + dateString);
}
private ObservableMap<KeyCombination, Runnable> accelerators() {
Scene scene = hideProcessesButton.getScene();
assert scene != null : "button not added to scene";
return scene.getAccelerators();
}
private void invertSelection(ToggleButton toggleButton) {
toggleButton.setSelected(!toggleButton.isSelected());
}
public BooleanProperty hideProcsProperty() {
return hideProcessesButton.selectedProperty();
}
public BooleanProperty hideFunctionsProperty() {
return hideFunctionsButton.selectedProperty();
}
private void toggleHideProcs() {
String buttonText = "";
if(hideProcessesButton.isSelected())
buttonText = "Show Processes";
else
buttonText = "Hide Processes";
hideProcessesButton.setText(buttonText);
}
private void toggleHideFuncs() {
String buttonText = "";
if(hideFunctionsButton.isSelected())
buttonText = "Show Modules";
else
buttonText = "Hide Modules";
hideFunctionsButton.setText(buttonText);
}
public final void setOnRefreshModules(EventHandler<ActionEvent> e) {
refreshModulesAction = e;
refreshModulesButton.setOnAction(refreshModulesAction);
}
public void disconnect() throws IOException, OtpErlangException{
ErlyBerly.nodeAPI().manuallyDisconnect();
ErlyBerly.nodeAPI().disconnect();
Stage s = new Stage();
displayConnectionPopup(s);
}
// TODO: (improve) lazy copy paste
// TODO: THIS was a ugly copy paste effort
private void displayConnectionPopup(Stage primaryStage) {
Stage connectStage;
connectStage = new Stage();
connectStage.initModality(Modality.WINDOW_MODAL);
connectStage.setScene(new Scene(new FxmlLoadable("/erlyberly/connection.fxml").load()));
connectStage.setAlwaysOnTop(true);
// javafx vertical resizing is laughably ugly, lets just disallow it
connectStage.setResizable(false);
connectStage.setWidth(400);
// if the user closes the window without connecting then close the app
connectStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent e) {
if(!ErlyBerly.nodeAPI().connectedProperty().get()) {
Platform.exit();
}
Platform.runLater(() -> {
//primaryStage.setResizable(true);
});
}});
connectStage.show();
}
private void displayPreferencesPane() {
ErlyBerly.showPreferencesPane();
}
class ErlangMemoryThread extends Thread {
private final ObservableList<Data> pieData;
public ErlangMemoryThread(ObservableList<PieChart.Data> thePieData) {
pieData = thePieData;
setName("Erlang Memory Thread");
setDaemon(true);
}
@Override
public void run() {
try {
final Map<Object, Object> erlangMemory = ErlyBerly.nodeAPI().erlangMemory();
// remove stats which are combinations of other stats
erlangMemory.remove(OtpUtil.atom("maximum"));
erlangMemory.remove(OtpUtil.atom("total"));
erlangMemory.remove(OtpUtil.atom("system"));
erlangMemory.remove(OtpUtil.atom("processes_used"));
erlangMemory.remove(OtpUtil.atom("atom_used"));
Platform.runLater(() -> {
populatePieData(erlangMemory);
});
}
catch (Exception e) {
e.printStackTrace();
}
}
private void populatePieData(final Map<Object, Object> erlangMemory) {
for (Entry<Object, Object> entry : erlangMemory.entrySet()) {
long kb = (long) (Double.parseDouble(entry.getValue().toString()) / 1024);
String label = entry.getKey().toString() + " (" + kb + " KB)";
pieData.add(new Data(label, kb));
}
}
}
}