/*********************************************************************************** * * Copyright (c) 2014 Kamil Baczkowicz * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Eclipse Distribution License v1.0 which accompany this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * * Kamil Baczkowicz - initial API and implementation and/or initial documentation * */ package pl.baczkowicz.mqttspy.ui; import java.net.URL; import java.util.List; import java.util.ResourceBundle; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import org.fxmisc.richtext.StyleClassedTextArea; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.baczkowicz.mqttspy.configuration.ConfigurationManager; import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage; import pl.baczkowicz.spy.common.generated.FormatterDetails; import pl.baczkowicz.spy.formatting.FormattingManager; import pl.baczkowicz.spy.formatting.FormattingUtils; import pl.baczkowicz.spy.ui.configuration.UiProperties; import pl.baczkowicz.spy.ui.controls.StyledTextAreaWrapper; import pl.baczkowicz.spy.ui.controls.TextAreaInterface; import pl.baczkowicz.spy.ui.controls.TextAreaWrapper; import pl.baczkowicz.spy.ui.events.MessageFormatChangeEvent; import pl.baczkowicz.spy.ui.events.MessageIndexChangeEvent; import pl.baczkowicz.spy.ui.search.SearchOptions; import pl.baczkowicz.spy.ui.storage.BasicMessageStoreWithSummary; import pl.baczkowicz.spy.utils.TimeUtils; /** * Controller for displaying a message. */ public class MessageController implements Initializable { final static Logger logger = LoggerFactory.getLogger(MessageController.class); @FXML private AnchorPane parentPane; @FXML private TextArea dataField; private TextAreaInterface dataFieldInteface; private StyleClassedTextArea styledDataField; @FXML private ToggleButton wrapToggle; @FXML private CheckBox retainedField; @FXML private TextField topicField; @FXML private TextField timeField; @FXML private TextField qosField; @FXML private Label dataLabel; @FXML private Label lengthLabel; @FXML private Label retainedFieldLabel; @FXML private Label qosFieldLabel; private BasicMessageStoreWithSummary<FormattedMqttMessage> store; private FormattedMqttMessage message; private FormatterDetails selectionFormat = null; private Tooltip tooltip; private Tooltip lengthTooltip; private SearchOptions searchOptions; private boolean detailedView; private ConfigurationManager configurationManager; private FormattingManager formattingManager; private boolean styled; @Override public void initialize(URL location, ResourceBundle resources) { // All done in init() as this is where the dataFieldInterface is assigned } public void init() { // dataField.heightProperty().addListener(new ChangeListener<Number>() // { // // @Override // public void changed(ObservableValue<? extends Number> observable, // Number oldValue, Number newValue) // { // logger.info("New height = {}; pane height = {}", newValue, parentPane.getHeight()); // // } // }); if (styled) { styledDataField = new StyleClassedTextArea(); AnchorPane.setBottomAnchor(styledDataField, AnchorPane.getBottomAnchor(dataField) - 1); AnchorPane.setLeftAnchor(styledDataField, AnchorPane.getLeftAnchor(dataField) - 1); AnchorPane.setTopAnchor(styledDataField, AnchorPane.getTopAnchor(dataField) - 1); AnchorPane.setRightAnchor(styledDataField, AnchorPane.getRightAnchor(dataField) - 1); parentPane.getChildren().add(styledDataField); parentPane.getChildren().remove(dataField); dataFieldInteface = new StyledTextAreaWrapper(styledDataField); } else { dataFieldInteface = new TextAreaWrapper(dataField); } dataFieldInteface.setEditable(false); dataFieldInteface.setWrapText(true); dataFieldInteface.selectedTextProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { updateTooltipText(); } }); dataLabel.setOnMouseClicked(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (event.getClickCount() == 2) { formattingManager.formatMessage(message, store.getFormatter()); final String textToDisplay = message.getPrettyPayload(); displayNewText(textToDisplay); } } }); tooltip = new Tooltip(""); tooltip.setWrapText(true); lengthTooltip = new Tooltip(); lengthLabel.setTooltip(lengthTooltip); } private void updateVisibility() { if (detailedView) { // Doing this so the panel doesn't get bigger AnchorPane.setRightAnchor(topicField, null); topicField.setPrefWidth(100); // Apply sizing and visibility AnchorPane.setRightAnchor(topicField, 342.0); } else { AnchorPane.setRightAnchor(topicField, 205.0); } qosField.setVisible(detailedView); retainedField.setVisible(detailedView); qosFieldLabel.setVisible(detailedView); retainedFieldLabel.setVisible(detailedView); lengthLabel.setVisible(detailedView); // TODO: basic perspective } public void setViewVisibility(final boolean detailedView) { this.detailedView = detailedView; updateVisibility(); } public void toggleDetailedViewVisibility() { detailedView = !detailedView; updateVisibility(); } public void onMessageIndexChange(final MessageIndexChangeEvent event) { updateMessage(event.getIndex()); } public void onFormatChange(final MessageFormatChangeEvent event) { showMessageData(); } private void updateMessage(final int messageIndex) { if (messageIndex > 0) { FormattedMqttMessage message = null; // Optimised for showing the latest message if (messageIndex == 1) { synchronized (store) { message = store.getMessages().get(0); populate(message); } } else { synchronized (store) { final List<FormattedMqttMessage> messages = store.getMessages(); // Make sure we don't try to re-display a message that is not in the store anymore if (messageIndex <= messages.size()) { message = messages.get(messageIndex - 1); populate(message); } } } } else { clear(); } } public void populate(final FormattedMqttMessage message) { // Don't populate with the same message object if (message != null && !message.equals(this.message)) { this.message = message; final String payload = new String(message.getPayload()); logger.trace("Message payload = " + payload); topicField.setText(message.getTopic()); qosField.setText(String.valueOf(message.getQoS())); timeField.setText(TimeUtils.DATE_WITH_MILLISECONDS_SDF.format(message.getDate())); retainedField.setSelected(message.isRetained()); // Take the length of the raw byte array populateLength(message.getRawMessage().getPayload().length); showMessageData(); } } private void populateLength(final long length) { populatePayloadLength(lengthLabel, lengthTooltip, length); } public static void populatePayloadLength(final Label lengthLabel, final Tooltip lengthTooltip, final long length) { if (lengthTooltip != null) { lengthTooltip.setText("Message length = " + length); } if (length < 1000) { lengthLabel.setText("(" + length + "B)"); } else { final long lengthInKB = length / 1000; if (lengthInKB < 1000) { lengthLabel.setText("(" + lengthInKB + "kB)"); } else { final long lengthInMB = lengthInKB / 1000; lengthLabel.setText("(" + lengthInMB + "MB)"); } } } public void clear() { this.message = null; topicField.setText(""); dataFieldInteface.clear(); qosField.setText(""); timeField.setText(""); lengthLabel.setText("(0)"); retainedField.setSelected(false); } public void formatSelection(final FormatterDetails messageFormat) { this.selectionFormat = messageFormat; if (selectionFormat != null) { updateTooltipText(); dataFieldInteface.setTooltip(tooltip); } else { dataFieldInteface.setTooltip(null); } } private void showMessageData() { if (message != null) { String textToDisplay = ""; // If large message detected if (message.getRawMessage().getPayload().length >= UiProperties.getLargeMessageSize(configurationManager.getUiPropertyFile())) { if (UiProperties.getLargeMessageHide(configurationManager.getUiPropertyFile())) { textToDisplay = "[message is too large and has been hidden - double click on 'Data' to display]"; } else { final int max = UiProperties.getLargeMessageSubstring(configurationManager.getUiPropertyFile()); formattingManager.formatMessage(message, store.getFormatter()); textToDisplay = message.getPrettyPayload().substring(0, max) + "... [message truncated to " + max + " characters - double click on 'Data' to display]"; } } else { // logger.debug("Formatting browsed message using {}", store.getFormatter().getName()); formattingManager.formatMessage(message, store.getFormatter()); textToDisplay = message.getPrettyPayload(); } displayNewText(textToDisplay); } } private void displayNewText(final String textToDisplay) { // Won't refresh the text if it is the same... if (!textToDisplay.equals(dataFieldInteface.getText())) { dataFieldInteface.clear(); dataFieldInteface.appendText(textToDisplay); dataFieldInteface.positionCaret(0); if (searchOptions != null && styled) { styledDataField.setStyleClass(0, dataFieldInteface.getText().length(), "messageText"); if (searchOptions.getSearchValue().length() > 0) { final String textToSearch = searchOptions.isMatchCase() ? dataFieldInteface.getText() : dataFieldInteface.getText().toLowerCase(); int pos = textToSearch.indexOf(searchOptions.getSearchValue()); while (pos >= 0) { styledDataField.setStyleClass(pos, pos + searchOptions.getSearchValue().length(), "messageTextHighlighted"); pos = textToSearch.indexOf(searchOptions.getSearchValue(), pos + 1); } } } updateTooltipText(); } } private void updateTooltipText() { if (selectionFormat != null) { final String tooltipText = FormattingUtils.checkAndFormatText(selectionFormat, dataFieldInteface.getSelectedText()); if (tooltipText.length() > 0) { tooltip.setText(tooltipText); } else { tooltip.setText("[select text to convert]"); } } } // =============================== // === Setters and getters ======= // =============================== public void setSearchOptions(final SearchOptions searchOptions) { this.searchOptions = searchOptions; } public void setConfingurationManager(final ConfigurationManager configurationManager) { this.configurationManager = configurationManager; } public void setFormattingManager(final FormattingManager formattingManager) { this.formattingManager = formattingManager; } public void setStore(final BasicMessageStoreWithSummary<FormattedMqttMessage> store) { this.store = store; } public void setStyled(final boolean styled) { this.styled = styled; } }