/*********************************************************************************** * * 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.io.File; import java.net.URL; 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.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuButton; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.HBox; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; import javafx.stage.Window; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage; import pl.baczkowicz.mqttspy.ui.messagelog.MessageLogUtils; import pl.baczkowicz.spy.eventbus.IKBus; import pl.baczkowicz.spy.files.FileUtils; import pl.baczkowicz.spy.messages.FormattedMessage; import pl.baczkowicz.spy.ui.events.MessageAddedEvent; import pl.baczkowicz.spy.ui.events.MessageIndexChangeEvent; import pl.baczkowicz.spy.ui.events.MessageIndexIncrementEvent; import pl.baczkowicz.spy.ui.events.MessageIndexToFirstEvent; import pl.baczkowicz.spy.ui.events.MessageRemovedEvent; import pl.baczkowicz.spy.ui.events.queuable.ui.BrowseRemovedMessageEvent; import pl.baczkowicz.spy.ui.storage.BasicMessageStoreWithSummary; import pl.baczkowicz.spy.ui.storage.ManagedMessageStoreWithFiltering; import pl.baczkowicz.spy.ui.utils.TextUtils; import pl.baczkowicz.spy.ui.utils.UiUtils; /** * Controller for the message navigation buttons. */ public class MessageNavigationController implements Initializable { final static Logger logger = LoggerFactory.getLogger(MessageNavigationController.class); @FXML private Label messageLabel; @FXML private Label filterStatusLabel; @FXML private CheckBox showLatestBox; @FXML private ToggleGroup wholeMessageFormat; @FXML private MenuButton formattingMenuButton; @FXML private Menu formatterMenu; @FXML private Menu customFormatterMenu; @FXML private CheckMenuItem uniqueOnlyMenu; @FXML private ToggleGroup selectionFormat; @FXML private Button moreRecentButton; @FXML private Button lessRecentButton; @FXML private Button showFirstButton; @FXML private Button showLastButton; @FXML private HBox messageIndexBox; @FXML private MenuButton filterButton; private int selectedMessage; private BasicMessageStoreWithSummary<FormattedMqttMessage> store; private TextField messageIndexValueField; private Label totalMessagesValueLabel; private IKBus eventBus; public void initialize(URL location, ResourceBundle resources) { messageIndexValueField = new TextField(); messageIndexValueField.setEditable(false); messageIndexValueField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> ob, String o, String n) { // expand the textfield messageIndexValueField.setPrefWidth(TextUtils.computeTextWidth( messageIndexValueField.getFont(), messageIndexValueField.getText()) + 12); } }); messageLabel.getStyleClass().add("messageIndex"); messageLabel.setPadding(new Insets(2, 2, 2, 2)); totalMessagesValueLabel = new Label(); totalMessagesValueLabel.getStyleClass().add("messageIndex"); totalMessagesValueLabel.setPadding(new Insets(2, 2, 2, 2)); filterStatusLabel = new Label(); filterStatusLabel.getStyleClass().add("filterOn"); filterStatusLabel.setPadding(new Insets(2, 2, 2, 2)); messageIndexValueField.setPadding(new Insets(2, 5, 2, 5)); messageIndexValueField.getStyleClass().add("messageIndex"); messageIndexValueField.addEventFilter(ScrollEvent.SCROLL, new EventHandler<ScrollEvent>() { @Override public void handle(ScrollEvent event) { switch(event.getTextDeltaYUnits()) { case LINES: updateMessageIndexFromScroll((int) event.getTextDeltaY()); break; case PAGES: updateMessageIndexFromScroll((int) event.getTextDeltaY()); break; case NONE: updateMessageIndexFromScroll((int) event.getDeltaY()); break; } } }); messageIndexValueField.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent keyEvent) { switch (keyEvent.getCode()) { case SPACE: { showLatestBox.setSelected(!showLatestBox.isSelected()); break; } case HOME: { showFirst(); break; } case END: { showLast(); break; } case PAGE_UP: { changeSelectedMessageIndex(5); break; } case PAGE_DOWN: { changeSelectedMessageIndex(-5); break; } case UP: { changeSelectedMessageIndex(1); break; } case DOWN: { changeSelectedMessageIndex(-1); break; } default: break; } } }); } public void init() { moreRecentButton.setTooltip(new Tooltip("Show more recent message")); lessRecentButton.setTooltip(new Tooltip("Show less recent message")); showFirstButton.setTooltip(new Tooltip("Show the latest message")); showLastButton.setTooltip(new Tooltip("Show the oldest message")); } // =================== // === FXML methods == // =================== @FXML private void showFirst() { showFirstMessage(); } @FXML private void showLast() { showLastMessage(); } @FXML private void showMoreRecent() { changeSelectedMessageIndex(-1); } @FXML private void showLessRecent() { changeSelectedMessageIndex(1); } // ==================== // === Other methods == // ==================== public void onMessageAdded(final MessageAddedEvent<FormattedMessage> event) { // This is registered for filtered messages only if (showLatest()) { onNavigateToFirst(new MessageIndexToFirstEvent(this)); } else { onMessageIndexIncrement(new MessageIndexIncrementEvent(event.getMessages().size(), store)); } } public void onMessageIndexChange(final MessageIndexChangeEvent event) { // Make sure this is not from itself if (event.getDispatcher() == this) { return; } // logger.info("{} Index change = " + newSelectedMessage, store.getName()); if (selectedMessage != event.getIndex()) { selectedMessage = event.getIndex(); updateIndex(); } } public void onNavigateToFirst(final MessageIndexToFirstEvent event) { // logger.info("{} Index change to first", store.getName()); showFirstMessage(); } public void onMessageIndexIncrement(final MessageIndexIncrementEvent event) { // logger.info("{} Index increment", store.getName()); selectedMessage = selectedMessage + event.getIncrement(); // Because this is an event saying a new message is available, but we don't want to display it, // so by not refreshing the content of the old one we allow uninterrupted interaction with UI fields (e.g. selection, etc.) updateIndex(false); } // TODO: optimise message handling public void onMessageRemoved(final MessageRemovedEvent<FormattedMessage> event) { for (final BrowseRemovedMessageEvent<FormattedMessage> message : event.getMessages()) { if (message.getMessageIndex() < selectedMessage) { selectedMessage--; } } updateIndex(false); } private void showFirstMessage() { if (store.getMessages().size() > 0) { selectedMessage = 1; updateIndex(); } else { selectedMessage = 0; updateIndex(); } } private void showLastMessage() { if (store.getMessages().size() > 0) { selectedMessage = store.getMessages().size(); updateIndex(); } } private void changeSelectedMessageIndex(final int count) { if (store.getMessages().size() > 0) { if (selectedMessage + count <= 1) { showFirstMessage(); } else if (selectedMessage + count >= store.getMessages().size()) { showLastMessage(); } else { selectedMessage = selectedMessage + count; updateIndex(); } } } private void updateIndex() { updateIndex(true); } private void updateIndex(final boolean refreshMessageDetails) { final String selectedIndexValue = selectedMessage > 0 ? String.valueOf(selectedMessage) : "-"; final String totalMessagesValue = "/ " + store.getMessages().size(); if (messageIndexBox.getChildren().size() == 1) { messageLabel.setText("Message "); messageIndexBox.getChildren().add(messageIndexValueField); messageIndexBox.getChildren().add(totalMessagesValueLabel); messageIndexBox.getChildren().add(filterStatusLabel); } messageIndexValueField.setText(selectedIndexValue); totalMessagesValueLabel.setText(totalMessagesValue); updateFilterStatus(); if (refreshMessageDetails) { eventBus.publish(new MessageIndexChangeEvent(selectedMessage, store, this)); // eventManager.changeMessageIndex(store, this, selectedMessage); } } private void updateFilterStatus() { if (!store.browsingFiltersEnabled()) { if (!store.messageFiltersEnabled()) { filterStatusLabel.setText(""); } else { filterStatusLabel.setText("(filter is active)"); } } else if (store instanceof ManagedMessageStoreWithFiltering) { if (!store.messageFiltersEnabled()) { filterStatusLabel.setText("(" + getBrowsingTopicsInfo((ManagedMessageStoreWithFiltering<FormattedMqttMessage>) store) + ")"); } else { filterStatusLabel.setText("(" + getBrowsingTopicsInfo((ManagedMessageStoreWithFiltering<FormattedMqttMessage>) store) + "; filter is active)"); } } } public static String getBrowsingTopicsInfo(final ManagedMessageStoreWithFiltering<FormattedMqttMessage> store) { final int selectedTopics = store.getFilteredMessageStore().getBrowsedTopics().size(); final int totalTopics = store.getAllTopics().size(); return "browsing " + selectedTopics + "/" + totalTopics + " " + (totalTopics == 1? "topic" : "topics"); } private void updateMessageIndexFromScroll(final int scroll) { if (scroll > 0) { changeSelectedMessageIndex(1); } else { changeSelectedMessageIndex(-1); } } public void clear() { messageLabel.setText("No messages"); messageIndexBox.getChildren().clear(); messageIndexBox.getChildren().add(messageLabel); } public boolean showLatest() { return showLatestBox.isSelected(); } public void hideShowLatest() { showLatestBox.setVisible(false); } public void copyMessageToClipboard() { if (getSelectedMessageIndex() > 0) { UiUtils.copyToClipboard(MessageLogUtils.getCurrentMessageAsMessageLog(store, getSelectedMessageIndex() - 1)); } } public void copyMessagesToClipboard() { UiUtils.copyToClipboard(MessageLogUtils.getAllMessagesAsMessageLog(store)); } public void copyMessageTopicToClipboard() { if (getSelectedMessageIndex() > 0) { final FormattedMqttMessage message = store.getMessages().get(getSelectedMessageIndex() - 1); UiUtils.copyToClipboard(message.getTopic()); } } public void copyMessageToFile() { if (getSelectedMessageIndex() > 0) { final FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Select message audit log file to save to"); String extensions = "messages"; fileChooser.setSelectedExtensionFilter(new ExtensionFilter("Message audit log file", extensions)); final File selectedFile = fileChooser.showSaveDialog(getParentWindow()); if (selectedFile != null) { FileUtils.writeToFile(selectedFile, MessageLogUtils.getCurrentMessageAsMessageLog(store, getSelectedMessageIndex() - 1)); } } } public void copyMessageToBinaryFile() { if (getSelectedMessageIndex() > 0) { final FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Select file to save to"); String extensions = "*"; fileChooser.setSelectedExtensionFilter(new ExtensionFilter("File", extensions)); final File selectedFile = fileChooser.showSaveDialog(getParentWindow()); if (selectedFile != null) { FileUtils.writeToFile(selectedFile, store.getMessages().get(getSelectedMessageIndex() - 1).getRawMessage().getPayload()); } } } public void copyMessagesToFile() { final FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Select message audit log file to save to"); String extensions = "messages"; fileChooser.setSelectedExtensionFilter(new ExtensionFilter("Message audit log file", extensions)); final File selectedFile = fileChooser.showSaveDialog(getParentWindow()); if (selectedFile != null) { FileUtils.writeToFile(selectedFile, MessageLogUtils.getAllMessagesAsMessageLog(store)); } } private Window getParentWindow() { return messageLabel.getScene().getWindow(); } // =============================== // === Setters and getters ======= // =============================== public void setStore(final BasicMessageStoreWithSummary<FormattedMqttMessage> store) { this.store = store; } public int getSelectedMessageIndex() { return selectedMessage; } /** * Get the 'unique only' menu item. * * @return the uniqueOnlyMenu */ public CheckMenuItem getUniqueOnlyMenu() { return uniqueOnlyMenu; } public void setViewVisibility(final boolean detailedView) { filterButton.setVisible(detailedView); } public void toggleDetaileledViewVisibility() { filterButton.setVisible(!filterButton.isVisible()); } public void setEventBus(final IKBus eventBus) { this.eventBus = eventBus; } }