/*********************************************************************************** * * 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.Arrays; import java.util.HashSet; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; import java.util.function.Predicate; import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Pos; import javafx.scene.control.ButtonType; import javafx.scene.control.ContextMenu; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.PropertyValueFactory; import javafx.util.Callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.baczkowicz.mqttspy.configuration.generated.TabbedSubscriptionDetails; import pl.baczkowicz.mqttspy.connectivity.BaseMqttSubscription; import pl.baczkowicz.mqttspy.connectivity.MqttAsyncConnection; import pl.baczkowicz.mqttspy.connectivity.MqttSubscription; import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage; import pl.baczkowicz.mqttspy.ui.charts.ChartFactory; import pl.baczkowicz.mqttspy.ui.charts.ChartMode; import pl.baczkowicz.mqttspy.ui.utils.StylingUtils; import pl.baczkowicz.spy.eventbus.IKBus; import pl.baczkowicz.spy.ui.events.MessageIndexToFirstEvent; import pl.baczkowicz.spy.ui.events.MessageListChangedEvent; import pl.baczkowicz.spy.ui.properties.SubscriptionTopicSummaryProperties; import pl.baczkowicz.spy.ui.storage.ManagedMessageStoreWithFiltering; import pl.baczkowicz.spy.ui.utils.DialogFactory; import pl.baczkowicz.spy.ui.utils.UiUtils; /** * Controller for the subscription summary table. */ @SuppressWarnings("rawtypes") public class SubscriptionSummaryTableController implements Initializable { private static final int CHART_TOPIC_COUNT = 10; private final static Logger logger = LoggerFactory.getLogger(SubscriptionSummaryTableController.class); private ManagedMessageStoreWithFiltering<FormattedMqttMessage> store; @FXML private TableView<SubscriptionTopicSummaryProperties> filterTable; @FXML private TableColumn<SubscriptionTopicSummaryProperties, Boolean> showColumn; @FXML private TableColumn<SubscriptionTopicSummaryProperties, String> topicColumn; @FXML private TableColumn<SubscriptionTopicSummaryProperties, String> contentColumn; @FXML private TableColumn<SubscriptionTopicSummaryProperties, Integer> messageCountColumn; @FXML private TableColumn<SubscriptionTopicSummaryProperties, String> lastReceivedColumn; private FilteredList<SubscriptionTopicSummaryProperties<FormattedMqttMessage>> filteredData; private ConnectionController connectionController; private IKBus eventBus; private Menu filteredTopicsMenu; private ObservableList<SubscriptionTopicSummaryProperties<FormattedMqttMessage>> nonFilteredData; private Set<String> shownTopics = new HashSet<>(); public void initialize(URL location, ResourceBundle resources) { // Table showColumn.setCellValueFactory(new PropertyValueFactory<SubscriptionTopicSummaryProperties, Boolean>( "show")); showColumn .setCellFactory(new Callback<TableColumn<SubscriptionTopicSummaryProperties, Boolean>, TableCell<SubscriptionTopicSummaryProperties, Boolean>>() { public TableCell<SubscriptionTopicSummaryProperties, Boolean> call( TableColumn<SubscriptionTopicSummaryProperties, Boolean> param) { final CheckBoxTableCell<SubscriptionTopicSummaryProperties, Boolean> cell = new CheckBoxTableCell<SubscriptionTopicSummaryProperties, Boolean>() { @Override public void updateItem(final Boolean checked, boolean empty) { super.updateItem(checked, empty); if (!isEmpty() && checked != null && this.getTableRow() != null && this.getTableRow().getItem() != null && store != null) { changeShowProperty((SubscriptionTopicSummaryProperties) this.getTableRow().getItem(), checked); } } }; cell.setAlignment(Pos.TOP_CENTER); return cell; } }); topicColumn.setCellValueFactory(new PropertyValueFactory<SubscriptionTopicSummaryProperties, String>("topic")); contentColumn.setCellValueFactory(new PropertyValueFactory<SubscriptionTopicSummaryProperties, String>("lastReceivedPayloadShort")); contentColumn.setCellFactory(new Callback<TableColumn<SubscriptionTopicSummaryProperties, String>, TableCell<SubscriptionTopicSummaryProperties, String>>() { public TableCell<SubscriptionTopicSummaryProperties, String> call( TableColumn<SubscriptionTopicSummaryProperties, String> param) { final TableCell<SubscriptionTopicSummaryProperties, String> cell = new TableCell<SubscriptionTopicSummaryProperties, String>() { @Override public void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (!isEmpty()) { setText(item); } else { setText(null); } } }; return cell; } }); messageCountColumn.setCellValueFactory(new PropertyValueFactory<SubscriptionTopicSummaryProperties, Integer>("count")); messageCountColumn.setCellFactory(new Callback<TableColumn<SubscriptionTopicSummaryProperties, Integer>, TableCell<SubscriptionTopicSummaryProperties, Integer>>() { public TableCell<SubscriptionTopicSummaryProperties, Integer> call( TableColumn<SubscriptionTopicSummaryProperties, Integer> param) { final TableCell<SubscriptionTopicSummaryProperties, Integer> cell = new TableCell<SubscriptionTopicSummaryProperties, Integer>() { @Override public void updateItem(Integer item, boolean empty) { super.updateItem(item, empty); if (!isEmpty()) { setText(item.toString()); } else { setText(null); } } }; cell.setAlignment(Pos.TOP_CENTER); return cell; } }); lastReceivedColumn.setCellValueFactory(new PropertyValueFactory<SubscriptionTopicSummaryProperties, String>("lastReceivedTimestamp")); lastReceivedColumn.setCellFactory(new Callback<TableColumn<SubscriptionTopicSummaryProperties, String>, TableCell<SubscriptionTopicSummaryProperties, String>>() { public TableCell<SubscriptionTopicSummaryProperties, String> call( TableColumn<SubscriptionTopicSummaryProperties, String> param) { final TableCell<SubscriptionTopicSummaryProperties, String> cell = new TableCell<SubscriptionTopicSummaryProperties, String>() { @Override public void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (!isEmpty()) { setText(item.toString()); } else { setText(null); } } }; cell.setAlignment(Pos.TOP_CENTER); return cell; } }); filterTable .setRowFactory(new Callback<TableView<SubscriptionTopicSummaryProperties>, TableRow<SubscriptionTopicSummaryProperties>>() { public TableRow<SubscriptionTopicSummaryProperties> call( TableView<SubscriptionTopicSummaryProperties> tableView) { final TableRow<SubscriptionTopicSummaryProperties> row = new TableRow<SubscriptionTopicSummaryProperties>() { @Override protected void updateItem(SubscriptionTopicSummaryProperties item, boolean empty) { super.updateItem(item, empty); if (!isEmpty() && item.getSubscription() != null) { final BaseMqttSubscription subscription = connectionController.getConnection().getMqttSubscriptionForTopic(item.getSubscription()); if (subscription instanceof MqttSubscription) { this.setStyle(StylingUtils.createBgRGBString( ((MqttSubscription) subscription).getColor(), getIndex() % 2 == 0 ? 0.8 : 0.6) + " -fx-background-radius: 6; "); } else { this.setStyle(null); } } else { this.setStyle(null); } } }; return row; } }); } private void changeShowProperty(final SubscriptionTopicSummaryProperties item, final boolean checked) { logger.trace("[{}] Show property changed; topic = {}, show value = {}", store.getName(), item.topicProperty().getValue(), checked); logger.trace("[{}] Store = {}, filtered store = {}", store.getName(), store.getNonFilteredMessageList(), store.getFilteredMessageStore().getMessageList()); if (store.getFilteredMessageStore().updateTopicFilter(item.topicProperty().getValue(), checked)) { // Wouldn't get updated properly if this is in the same thread Platform.runLater(new Runnable() { @Override public void run() { eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); eventBus.publish(new MessageListChangedEvent(store.getMessageList())); // eventManager.notifyMessageListChanged(store.getMessageList()); } }); } } public void init() { filterTable.setContextMenu(createTopicTableContextMenu(connectionController.getConnection())); nonFilteredData = store.getNonFilteredMessageList().getTopicSummary().getObservableMessagesPerTopic(); // Create filtered data set filteredData = new FilteredList<>(nonFilteredData); // Create sortable data set final SortedList<SubscriptionTopicSummaryProperties> sortedData = new SortedList<>(filteredData); // Bind the sortable list with the table sortedData.comparatorProperty().bind(filterTable.comparatorProperty()); // Set the data on the table filterTable.setItems(sortedData); filteredData.addListener(new ListChangeListener<SubscriptionTopicSummaryProperties>() { @Override public void onChanged(Change<? extends SubscriptionTopicSummaryProperties> c) { filteredTopicsMenu.setDisable(filteredData.size() == nonFilteredData.size()); } }); } public void refreshRowStyling() { // To refresh the styling, add and remove an invisible column final TableColumn<SubscriptionTopicSummaryProperties, String> column = new TableColumn<>(); column.setMaxWidth(0); column.setPrefWidth(0); filterTable.getColumns().add(column); filterTable.getColumns().remove(column); } public int getFilteredDataSize() { return filteredData.size(); } public void updateTopicFilter(final String topicFilter) { synchronized (filteredData) { filteredData.setPredicate(new Predicate<SubscriptionTopicSummaryProperties>() { @Override public boolean test(final SubscriptionTopicSummaryProperties item) { // If filter text is empty, display all persons. if (topicFilter == null || topicFilter.isEmpty()) { return true; } final String topic = item.topicProperty().getValue(); if (topic.toLowerCase().indexOf(topicFilter.toLowerCase()) != -1) { // Filter matches first name synchronized (shownTopics) { shownTopics.add(topic); } return true; } // Does not match synchronized (shownTopics) { shownTopics.remove(topic); } return false; } }); } } private void showChartsWindow(final Set<String> topics, final ChartMode mode, final MqttAsyncConnection connection) { final String connectionName = connection != null ? " - " + connection.getName() : ""; if (ChartMode.USER_DRIVEN_MSG_SIZE.equals(mode)) { new ChartFactory<FormattedMqttMessage>().createMessageBasedLineChart(topics, store, mode, "Topic", "Size", "bytes", "Message size chart" + connectionName, filterTable.getScene(), eventBus); } else { new ChartFactory<FormattedMqttMessage>().createMessageBasedLineChart(topics, store, mode, "Topic", "Value", "", "Message content chart" + connectionName, filterTable.getScene(), eventBus); } } public ContextMenu createTopicTableContextMenu(final MqttAsyncConnection connection) { final ContextMenu contextMenu = new ContextMenu(); // Subscribe to topic final MenuItem subscribeToTopicItem = new MenuItem("Subscribe (and create tab)"); subscribeToTopicItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { final TabbedSubscriptionDetails subscriptionDetails = new TabbedSubscriptionDetails(); subscriptionDetails.setTopic(item.topicProperty().getValue()); subscriptionDetails.setQos(0); connectionController.getNewSubscriptionPaneController().subscribe(subscriptionDetails, true); } } }); contextMenu.getItems().add(subscribeToTopicItem); // Separator contextMenu.getItems().add(new SeparatorMenuItem()); // Copy topic final MenuItem copyTopicItem = new MenuItem("Copy topic to clipboard"); copyTopicItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { UiUtils.copyToClipboard(item.topicProperty().getValue()); } } }); contextMenu.getItems().add(copyTopicItem); // Copy content final MenuItem copyContentItem = new MenuItem("Copy message content to clipboard"); copyContentItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { UiUtils.copyToClipboard(item.lastReceivedPayloadProperty().getValue()); } } }); contextMenu.getItems().add(copyContentItem); // Separator contextMenu.getItems().add(new SeparatorMenuItem()); // All filters final Menu allTopicsMenu = new Menu("Browse all topics"); // Apply all filters final MenuItem selectAllTopicsItem = new MenuItem("Select all topics"); selectAllTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.setAllShowValues(true); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); allTopicsMenu.getItems().add(selectAllTopicsItem); // Toggle all filters final MenuItem toggleAllTopicsItem = new MenuItem("Toggle all topics"); toggleAllTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.toggleAllShowValues(); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); allTopicsMenu.getItems().add(toggleAllTopicsItem); // Remove all filters final MenuItem removeAllTopicsItem = new MenuItem("Deselect all topics"); removeAllTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.setAllShowValues(false); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); allTopicsMenu.getItems().add(removeAllTopicsItem); contextMenu.getItems().add(allTopicsMenu); // Filtered topics filteredTopicsMenu = new Menu("Browse filtered topics"); // Apply filtered filters final MenuItem selectFilteredTopicsItem = new MenuItem("Add filtered topics to selection"); selectFilteredTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.setShowValues(true, shownTopics); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); filteredTopicsMenu.getItems().add(selectFilteredTopicsItem); // Clear and add filtered filters final MenuItem removeAllAndAddFilteredTopicsItem = new MenuItem("Deselect all and selected filtered topics"); removeAllAndAddFilteredTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel().getSelectedItem(); if (item != null) { store.setAllShowValues(false); store.setShowValues(true, shownTopics); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); filteredTopicsMenu.getItems().add(removeAllAndAddFilteredTopicsItem); // Toggle filtered filters final MenuItem toggleFilteredTopicsItem = new MenuItem("Toggle selection for filtered topics"); toggleFilteredTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.toggleShowValues(shownTopics); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); filteredTopicsMenu.getItems().add(toggleFilteredTopicsItem); // Remove filtered filters final MenuItem removeFilteredTopicsItem = new MenuItem("Deselect filtered topics"); removeFilteredTopicsItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.setShowValues(false, shownTopics); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); filteredTopicsMenu.getItems().add(removeFilteredTopicsItem); contextMenu.getItems().add(filteredTopicsMenu); // Only this topic final MenuItem selectOnlyThisItem = new MenuItem("Browse & select only this topic"); selectOnlyThisItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel() .getSelectedItem(); if (item != null) { store.setAllShowValues(false); store.setShowValue(item.topicProperty().getValue(), true); eventBus.publish(new MessageIndexToFirstEvent(store)); // eventManager.navigateToFirst(store); } } }); contextMenu.getItems().add(selectOnlyThisItem); // Separator contextMenu.getItems().add(new SeparatorMenuItem()); // Charts Menu chartsItem = new Menu("Charts"); MenuItem chartPayloadItem = new MenuItem("Show payload values for this topic"); chartPayloadItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel().getSelectedItem(); if (item != null) { final String topic = item.topicProperty().getValue(); showChartsWindow( new HashSet<String>(Arrays.asList(topic)), ChartMode.USER_DRIVEN_MSG_PAYLOAD, connection); } } }); chartsItem.getItems().add(chartPayloadItem); MenuItem chartPayloadForAllSelectedItem = new MenuItem("Show payload values for browsed topics"); chartPayloadForAllSelectedItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel().getSelectedItem(); if (item != null) { final Set<String> topics = store.getFilteredMessageStore().getBrowsedTopics(); if (topics.size() > CHART_TOPIC_COUNT) { final Optional<ButtonType> response = DialogFactory.createQuestionDialog( "Number of selected topics", "More than " + CHART_TOPIC_COUNT + " topics have been selected to be displayed on a chart. Do you want to proceed?", false); if (response.get() != ButtonType.YES) { return; } } showChartsWindow(topics, ChartMode.USER_DRIVEN_MSG_PAYLOAD, connection); } } }); chartsItem.getItems().add(chartPayloadForAllSelectedItem); // Separator chartsItem.getItems().add(new SeparatorMenuItem()); MenuItem chartSizeItem = new MenuItem("Show payload size for this topic"); chartSizeItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel().getSelectedItem(); if (item != null) { final String topic = item.topicProperty().getValue(); showChartsWindow(new HashSet<String>(Arrays.asList(topic)), ChartMode.USER_DRIVEN_MSG_SIZE, connection); } } }); chartsItem.getItems().add(chartSizeItem); MenuItem chartSizeForAllSelectedItem = new MenuItem("Show payload size for browsed topics"); chartSizeForAllSelectedItem.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent e) { final SubscriptionTopicSummaryProperties item = filterTable.getSelectionModel().getSelectedItem(); if (item != null) { final Set<String> topics = store.getFilteredMessageStore().getBrowsedTopics(); if (topics.size() > CHART_TOPIC_COUNT) { final Optional<ButtonType> response = DialogFactory.createQuestionDialog( "Number of selected topics", "More than " + CHART_TOPIC_COUNT + " topics have been selected to be displayed on a chart. Do you want to proceed?", false); if (response.get() != ButtonType.YES) { return; } } showChartsWindow(topics, ChartMode.USER_DRIVEN_MSG_SIZE, connection); } } }); chartsItem.getItems().add(chartSizeForAllSelectedItem); contextMenu.getItems().add(chartsItem); return contextMenu; } public void setStore(final ManagedMessageStoreWithFiltering<FormattedMqttMessage> store) { this.store = store; } public void setConnectionController(final ConnectionController connectionController) { this.connectionController = connectionController; } /** * Gets the shown/filtered topics. * * @return the shownTopics */ public Set<String> getShownTopics() { return shownTopics; } public void setEventBus(final IKBus eventBus) { this.eventBus = eventBus; } }