diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/Messages.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/Messages.java index 6ae1f35e8f..e1a7306a08 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/Messages.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/Messages.java @@ -233,6 +233,7 @@ public class Messages PointTypeTT, PosErrColumn, PropertiesTabName, + PVColumn, PVName, PVUsedInFormulaFmt, Refresh, @@ -246,7 +247,19 @@ public class Messages RequestTypeWarning, RequestTypeWarningDetail, SampleView_Count, + SampleView_Count_Visible, + SampleView_Filter_Change, + SampleView_FilterTypeTT, + SampleView_FilterValueTT, SampleView_Item, + SampleView_ItemFilter_ALARM_CHANGES, + SampleView_ItemFilter_ALARM_UP, + SampleView_ItemFilter_NO_FILTER, + SampleView_ItemFilter_THRESHOLD_CHANGES, + SampleView_ItemFilter_THRESHOLD_DOWN, + SampleView_ItemFilter_THRESHOLD_UP, + SampleView_Move_Down, + SampleView_Move_Up , SampleView_Refresh, SampleView_RefreshTT, SampleView_SelectItem, @@ -291,6 +304,10 @@ public class Messages TraceLineStyleTT, TraceLineWidth, TraceLineWidthTT, + TracesFilterType, + TracesFilterTypeTT, + TracesFilterValue, + TracesFilterValueTT, TracesTab, TraceTableEmpty, TraceType, diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/ModelItem.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/ModelItem.java index 12cc766d2b..0dfe40f107 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/ModelItem.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/ModelItem.java @@ -20,6 +20,7 @@ import org.csstudio.javafx.rtplot.data.PlotDataItem; import org.csstudio.trends.databrowser3.persistence.XMLPersistence; import org.csstudio.trends.databrowser3.preferences.Preferences; +import org.csstudio.trends.databrowser3.ui.sampleview.ItemSampleViewFilter; import org.phoebus.framework.persistence.XMLUtil; import org.w3c.dom.Element; @@ -84,6 +85,9 @@ abstract public class ModelItem */ private String uniqueId; + /** Item specific settings for SampleView filter*/ + private ItemSampleViewFilter sample_view_filter = new ItemSampleViewFilter(); + /** Initialize * @param name Name of the PV or the formula */ @@ -359,6 +363,25 @@ public void setWaveformIndex(int index) // Do nothing. } + /** + * + * @return Current SampleView Filter + */ + public ItemSampleViewFilter getSampleViewFilter() + { + return sample_view_filter; + } + + /** + * + * @param sample_view_filter New Filter object to be used for the SampleView display of this item + */ + public void setSampleViewFilter(ItemSampleViewFilter sample_view_filter) + { + this.sample_view_filter = sample_view_filter; + fireItemLookChanged(); + } + /** @return Samples held by this item */ abstract public PlotSamples getSamples(); @@ -427,6 +450,14 @@ protected void writeCommonConfig(final XMLStreamWriter writer) throws Exception writer.writeStartElement(XMLPersistence.TAG_WAVEFORM_INDEX); writer.writeCharacters(Integer.toString(getWaveformIndex())); writer.writeEndElement(); + + writer.writeStartElement(XMLPersistence.TAG_FILTER_TYPE); + writer.writeCharacters(getSampleViewFilter().getFilterType().name()); + writer.writeEndElement(); + + writer.writeStartElement(XMLPersistence.TAG_FILTER_VALUE); + writer.writeCharacters(Double.toString(getSampleViewFilter().getFilterValue())); + writer.writeEndElement(); } /** Load common XML configuration elements into this item @@ -490,6 +521,31 @@ else if (type.equals("CROSSES")) } } setWaveformIndex(XMLUtil.getChildInteger(node, XMLPersistence.TAG_WAVEFORM_INDEX).orElse(0)); + + try + { + String filter_type_from_document = + XMLUtil.getChildString(node, XMLPersistence.TAG_FILTER_TYPE) + .orElse(ItemSampleViewFilter.FilterType.NO_FILTER.name()); + sample_view_filter.setFilterType(ItemSampleViewFilter.FilterType.valueOf(filter_type_from_document)); + } + catch (Throwable ex) + { + sample_view_filter.setFilterType(ItemSampleViewFilter.FilterType.NO_FILTER); // Default to no filter + } + + try + { + double filter_value_from_document = + XMLUtil.getChildDouble(node, XMLPersistence.TAG_FILTER_VALUE) + .orElse(0.0); + sample_view_filter.setFilterValue(filter_value_from_document); + } + catch (Throwable ex) + { + sample_view_filter.setFilterValue(0.0); // Default to 0.0 + } + } /** Dispose all data */ diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java index a6882b1eb4..9ca8363f11 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java @@ -119,6 +119,8 @@ public class XMLPersistence TAG_POINT_TYPE = "point_type", TAG_POINT_SIZE = "point_size", TAG_WAVEFORM_INDEX = "waveform_index", + TAG_FILTER_TYPE = "filter_type", + TAG_FILTER_VALUE = "filter_value", TAG_SCAN_PERIOD = "period", TAG_LIVE_SAMPLE_BUFFER_SIZE = "ring_size", TAG_REQUEST = "request", diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/Perspective.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/Perspective.java index f0c53d426d..f119776473 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/Perspective.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/Perspective.java @@ -11,13 +11,14 @@ import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import java.util.stream.Collectors; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; import org.csstudio.trends.databrowser3.Activator; import org.csstudio.trends.databrowser3.Messages; import org.csstudio.trends.databrowser3.imports.SampleImportAction; @@ -32,6 +33,7 @@ import org.csstudio.trends.databrowser3.ui.properties.PropertyPanel; import org.csstudio.trends.databrowser3.ui.properties.RemoveUnusedAxes; import org.csstudio.trends.databrowser3.ui.sampleview.SampleView; +import org.csstudio.trends.databrowser3.ui.sampleview.ToggleSampleViewPositionMenuItem; import org.csstudio.trends.databrowser3.ui.search.SearchView; import org.csstudio.trends.databrowser3.ui.selection.DatabrowserSelection; import org.csstudio.trends.databrowser3.ui.waveformview.WaveformView; @@ -71,28 +73,35 @@ public class Perspective extends SplitPane SHOW_SEARCH = "show_search", SHOW_PROPERTIES = "show_properties", SHOW_EXPORT = "show_export", - SHOW_WAVEFORM = "show_waveform"; + SHOW_WAVEFORM = "show_waveform", + SHOW_SAMPLEVIEW = "show_sampleview", + SAMPLEVIEW_IN_BOTTOM_TABS = "sampleview_in_bottom_tabs"; private static final Preferences prefs = PhoebusPreferenceService.userNodeForClass(Perspective.class); private final Model model = new Model(); private final ModelBasedPlot plot = new ModelBasedPlot(true); - private SearchView search; + private SearchView search = null; private ExportView export = null; - private SampleView inspect = null; + private SampleView sampleview = null; private WaveformView waveform = null; + private final PropertyPanel property_panel; + private final Controller controller; private final TabPane left_tabs = new TabPane(), bottom_tabs = new TabPane(); - private final SplitPane plot_and_tabs = new SplitPane(plot.getPlot(), bottom_tabs); - private PropertyPanel property_panel; - private Tab search_tab, properties_tab, export_tab, inspect_tab, waveform_tab = null; + private final Pane top_pane = new StackPane(); + private final SplitPane plot_and_tabs = new SplitPane(top_pane, bottom_tabs); + private Tab search_tab, properties_tab, export_tab, sampleview_tab, waveform_tab; /** @param minimal Only show the essentials? */ public Perspective(final boolean minimal) { + top_pane.getChildren().setAll(plot.getPlot()); + + property_panel = new PropertyPanel(model, plot.getPlot().getUndoableActionManager()); properties_tab = new Tab(Messages.PropertiesTabName, property_panel); properties_tab.setGraphic(Activator.getIcon("properties")); @@ -174,8 +183,8 @@ private void createContextMenu() final MenuItem show_samples = new MenuItem(Messages.InspectSamples, Activator.getIcon("search")); show_samples.setOnAction(event -> { - createInspectionTab(); - showBottomTab(inspect_tab); + createSampleViewTab(plot.getPlot().getUndoableActionManager()); + showBottomTab(sampleview_tab); }); final MenuItem show_waveform = new MenuItem(Messages.OpenWaveformView, Activator.getIcon("wavesample")); @@ -202,7 +211,7 @@ private void createContextMenu() items.add(new PrintAction(plot.getPlot().getCenter())); items.add(new SaveSnapshotAction(plot.getPlot().getCenter())); - SelectionService.getInstance().setSelection(this, Arrays.asList(DatabrowserSelection.of(model, plot))); + SelectionService.getInstance().setSelection(this, List.of(DatabrowserSelection.of(model, plot))); List supported = ContextMenuService.getInstance().listSupportedContextMenuEntries(); supported.stream().forEach(action -> { MenuItem menuItem = new MenuItem(action.getName(), new ImageView(action.getIcon())); @@ -227,6 +236,57 @@ private void createContextMenu() }); } + private void createSampleViewContextMenu() { + final ContextMenu menu = new ContextMenu(); + final ObservableList items = menu.getItems(); + + final UndoableActionManager undo = plot.getPlot().getUndoableActionManager(); + + final List add_data = new ArrayList<>(); + add_data.add(new AddPVorFormulaMenuItem(sampleview, model, undo, false)); + add_data.add(new AddPVorFormulaMenuItem(sampleview, model, undo, true)); + + for (String type : SampleImporters.getTypes()) + add_data.add(new SampleImportAction(model, type, undo)); + + final MenuItem show_search = new MenuItem(Messages.OpenSearchView, Activator.getIcon("search")); + show_search.setOnAction(event -> showSearchTab()); + + final MenuItem show_properties = new MenuItem(Messages.OpenPropertiesView, Activator.getIcon("properties")); + show_properties.setOnAction(event -> + { + // Update pref that properties were last opened + prefs.putBoolean(SHOW_PROPERTIES, true); + showBottomTab(properties_tab); + }); + + final MenuItem show_export = new MenuItem(Messages.OpenExportView, Activator.getIcon("export")); + show_export.setOnAction(event -> + { + createExportTab(); + showBottomTab(export_tab); + }); + + final MenuItem show_waveform = new MenuItem(Messages.OpenWaveformView, Activator.getIcon("wavesample")); + show_waveform.setOnAction(event -> + { + createWaveformTab(); + showBottomTab(waveform_tab); + }); + final MenuItem refresh = new MenuItem(Messages.Refresh, Activator.getIcon("refresh_remote")); + refresh.setOnAction(event -> sampleview.update()); + + + sampleview.setOnContextMenuRequested(event -> { + items.clear(); + items.addAll(new ToggleSampleViewPositionMenuItem(this), new SeparatorMenuItem()); + items.addAll(add_data); + items.addAll(new SeparatorMenuItem(), show_search, show_properties, show_export, show_waveform, refresh); + + menu.show(getScene().getWindow(), event.getScreenX(), event.getScreenY()); + }); + } + private void createSearchTab() { if (search_tab == null) @@ -249,17 +309,35 @@ private void createExportTab() } } - private void createInspectionTab() + private void createSampleViewTab(UndoableActionManager undo) { - if (inspect_tab == null) + if (sampleview_tab == null) { - inspect = new SampleView(model); - inspect_tab = new Tab(Messages.InspectSamples, inspect); - inspect_tab.setGraphic(Activator.getIcon("search")); - inspect_tab.setOnClosed(evt -> autoMinimizeBottom()); + sampleview = new SampleView(model, undo); + sampleview_tab = new Tab(Messages.InspectSamples, sampleview); + sampleview_tab.setGraphic(Activator.getIcon("search")); + sampleview_tab.setOnClosed(evt -> autoMinimizeBottom()); + + createSampleViewContextMenu(); } } + // Gets called from the SampleView contextmenu, so we can assume it's not null + public void setSampleviewLocation(boolean set_in_bottom_tabs) { + if (set_in_bottom_tabs) { + top_pane.getChildren().setAll(plot.getPlot()); + showBottomTab(sampleview_tab); + } + else { + bottom_tabs.getTabs().remove(sampleview_tab); + top_pane.getChildren().setAll(sampleview); + } + } + + public boolean isSampleViewInBottomTabs() { + return bottom_tabs.getTabs().contains(sampleview_tab); + } + private void createWaveformTab() { if (waveform_tab == null) @@ -442,6 +520,17 @@ public void restore(final Memento memento) } }); + memento.getBoolean(SHOW_SAMPLEVIEW).ifPresent(show -> + { + if (show) + { + createSampleViewTab(plot.getPlot().getUndoableActionManager()); + memento.getBoolean(SAMPLEVIEW_IN_BOTTOM_TABS).ifPresent(in_bottom_tabs -> { + setSampleviewLocation(in_bottom_tabs); + }); + } + }); + // Has no effect when run right now? Platform.runLater(() -> { @@ -473,6 +562,7 @@ public void save(final Memento memento) if (left_tabs.getTabs().contains(search_tab)) memento.setBoolean(SHOW_SEARCH, true); + // properties open by default. save only if closed if (! bottom_tabs.getTabs().contains(properties_tab)) memento.setBoolean(SHOW_PROPERTIES, false); @@ -481,6 +571,15 @@ public void save(final Memento memento) if (bottom_tabs.getTabs().contains(waveform_tab)) memento.setBoolean(SHOW_WAVEFORM, true); + + if (top_pane.getChildren().contains(sampleview)) { + memento.setBoolean(SHOW_SAMPLEVIEW, true); + memento.setBoolean(SAMPLEVIEW_IN_BOTTOM_TABS, false); + } + if (bottom_tabs.getTabs().contains(sampleview_tab)) { + memento.setBoolean(SHOW_SAMPLEVIEW, true); + memento.setBoolean(SAMPLEVIEW_IN_BOTTOM_TABS, true); + } } /** Reclaim resources */ diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/ChangeSampleViewFilterCommand.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/ChangeSampleViewFilterCommand.java new file mode 100644 index 0000000000..41a0568798 --- /dev/null +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/ChangeSampleViewFilterCommand.java @@ -0,0 +1,39 @@ +package org.csstudio.trends.databrowser3.ui.properties; + + +import org.csstudio.trends.databrowser3.Messages; +import org.csstudio.trends.databrowser3.model.ModelItem; +import org.csstudio.trends.databrowser3.ui.sampleview.ItemSampleViewFilter; +import org.phoebus.ui.undo.UndoableAction; +import org.phoebus.ui.undo.UndoableActionManager; + +/** + * UNdo-able command to change item's display filters in the sample view + * @author Thomas Lehrach + */ +public class ChangeSampleViewFilterCommand extends UndoableAction { + + final private ModelItem item; + final private ItemSampleViewFilter old_filter, new_filter; + + public ChangeSampleViewFilterCommand(final UndoableActionManager operations_manager, + final ModelItem item, final ItemSampleViewFilter new_filter) { + super(Messages.SampleView_Filter_Change); + this.item = item; + this.old_filter = item.getSampleViewFilter(); + this.new_filter = new_filter; + operations_manager.execute(this); + } + + @Override + public void run() { + item.setSampleViewFilter(new_filter); + // Set filter in model item to new + } + + @Override + public void undo() { + item.setSampleViewFilter(old_filter); + // Set filter in model item to old + } +} diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/TracesTab.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/TracesTab.java index 327b558063..981e361e96 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/TracesTab.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/TracesTab.java @@ -15,6 +15,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import javafx.util.converter.NumberStringConverter; import org.csstudio.javafx.rtplot.LineStyle; import org.csstudio.javafx.rtplot.PointType; import org.csstudio.javafx.rtplot.TraceType; @@ -30,6 +31,7 @@ import org.csstudio.trends.databrowser3.model.PlotSample; import org.csstudio.trends.databrowser3.model.RequestType; import org.csstudio.trends.databrowser3.preferences.Preferences; +import org.csstudio.trends.databrowser3.ui.sampleview.ItemSampleViewFilter; import org.phoebus.archive.vtype.DefaultVTypeFormat; import org.phoebus.core.types.ProcessVariable; import org.phoebus.framework.selection.SelectionService; @@ -93,6 +95,7 @@ public class TracesTab extends Tab private static final List trace_types = List.of(TraceType.getDisplayNames()); private static final List line_styles = List.of(LineStyle.getDisplayNames()); private static final List point_types = List.of(PointType.getDisplayNames()); + private static final List filter_types = List.of(ItemSampleViewFilter.FilterType.getDisplayNames()); private final Model model; @@ -682,6 +685,53 @@ public void updateItem(String value, boolean empty) PropertyPanel.addTooltip(col, Messages.WaveformIndexColTT); trace_table.getColumns().add(col); + // Filter Type Column ---------- + col = new TableColumn<>(Messages.TracesFilterType); + col.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getSampleViewFilter().getFilterType().toString())); + col.setCellFactory(cell -> new DirectChoiceBoxTableCell<>(FXCollections.observableArrayList(filter_types))); + col.setOnEditCommit(event -> + { + final int index = filter_types.indexOf(event.getNewValue()); + final ItemSampleViewFilter.FilterType type = ItemSampleViewFilter.FilterType.values()[index]; + + // Make a copy so that we can undo the change + final ItemSampleViewFilter filter = new ItemSampleViewFilter(event.getRowValue().getSampleViewFilter()); + filter.setFilterType(type); + + new ChangeSampleViewFilterCommand(undo, event.getRowValue(), filter); + }); + + PropertyPanel.addTooltip(col, Messages.TracesFilterTypeTT); + trace_table.getColumns().add(col); + + + // Filter Value Column ---------- + col = new TableColumn<>(Messages.TracesFilterValue); + col.setCellValueFactory(cell -> + new SimpleStringProperty(Double.toString(cell.getValue().getSampleViewFilter().getFilterValue()))); + col.setCellFactory(TextFieldTableCell.forTableColumn()); + col.setOnEditCommit(event -> + { + final ModelItem item = event.getRowValue(); + try + { + // Make a copy so that we can undo the change + final ItemSampleViewFilter filter = new ItemSampleViewFilter(item.getSampleViewFilter()); + filter.setFilterValue(Double.parseDouble(event.getNewValue())); + + new ChangeSampleViewFilterCommand(undo, item, filter); + } + catch (Exception e) + { + trace_table.refresh(); + } + }); + + PropertyPanel.addTooltip(col, Messages.TracesFilterValueTT); + trace_table.getColumns().add(col); + + trace_table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); trace_table.setEditable(true); trace_table.getColumns().forEach(c -> c.setSortable(false)); diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/sampleview/ItemSampleViewFilter.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/sampleview/ItemSampleViewFilter.java new file mode 100644 index 0000000000..70020e3dca --- /dev/null +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/sampleview/ItemSampleViewFilter.java @@ -0,0 +1,76 @@ +package org.csstudio.trends.databrowser3.ui.sampleview; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +import static org.csstudio.trends.databrowser3.Messages.*; + +public class ItemSampleViewFilter { + public enum FilterType { + NO_FILTER(SampleView_ItemFilter_NO_FILTER), + ALARM_UP(SampleView_ItemFilter_ALARM_UP), + ALARM_CHANGES(SampleView_ItemFilter_ALARM_CHANGES), + THRESHOLD_UP(SampleView_ItemFilter_THRESHOLD_UP), + THRESHOLD_CHANGES(SampleView_ItemFilter_THRESHOLD_CHANGES), + THRESHOLD_DOWN(SampleView_ItemFilter_THRESHOLD_DOWN); + + private final String label; + + FilterType(String label) { + this.label = label; + } + + /** @return Array of display names for all Filter types */ + public static String[] getDisplayNames() { + final FilterType[] types = FilterType.values(); + final String[] names = new String[types.length]; + for (int i=0; i items = new ComboBox<>(); + private final UndoableActionManager undo; + private final ComboBox items = new ComboBox<>(); + private final ComboBox filter_type = new ComboBox<>(); + private final TextField filter_value = new TextField(); private final Label sample_count = new Label(Messages.SampleView_Count); - private final TableView sample_table = new TableView<>(); - private volatile ModelItem modelItem = null; + private final TableView sample_table = new TableView<>(); + private volatile ModelItemListItem modelItem = null; // Wrapped ModelItem for the combobox to display all samples from all PVs + private final ObservableList samples = FXCollections.observableArrayList(); + private final SortedList sorted_samples = new SortedList<>(samples); + private int all_samples_size; - private static class SeverityColoredTableCell extends TableCell { + private static class SeverityColoredTableCell extends TableCell { @Override protected void updateItem(final String item, final boolean empty) { super.updateItem(item, empty); - final TableRow row = getTableRow(); + final TableRow row = getTableRow(); if (empty || row == null || row.getItem() == null) setText(""); else { @@ -70,11 +71,35 @@ protected void updateItem(final String item, final boolean empty) { } } + private final ModelListener model_listener = new ModelListener() { + @Override + public void itemAdded(ModelItem item) { + update(); + } + + @Override + public void itemRemoved(ModelItem item) { + update(); + } + + @Override + public void changedItemLook(ModelItem item) { + update(); + } + + @Override + public void itemRefreshRequested(PVItem item) { + update(); + } + }; + /** * @param model Model */ - public SampleView(final Model model) { + public SampleView(final Model model, UndoableActionManager undo) { this.model = model; + this.undo = undo; + model.addListener(model_listener); items.setOnAction(event -> select(items.getSelectionModel().getSelectedItem())); @@ -87,9 +112,33 @@ public SampleView(final Model model) { refresh.setOnAction(event -> update()); final Label label = new Label(Messages.SampleView_Item); - final HBox top_row = new HBox(5, label, items, refresh); + final HBox top_row = new HBox(8, label, items, refresh); top_row.setAlignment(Pos.CENTER_LEFT); + filter_type.setTooltip(new Tooltip(Messages.SampleView_FilterTypeTT)); + filter_type.getItems().setAll(ItemSampleViewFilter.FilterType.values()); + if (modelItem != null) + filter_type.setValue(this.modelItem.getModelItem().getSampleViewFilter().getFilterType()); + filter_type.setOnAction(event -> { + final ItemSampleViewFilter filter = new ItemSampleViewFilter(modelItem.getModelItem().getSampleViewFilter()); + filter.setFilterType(filter_type.getValue()); + + new ChangeSampleViewFilterCommand(undo, modelItem.getModelItem(), filter); + }); + + filter_value.setTooltip(new Tooltip(Messages.SampleView_FilterValueTT)); + filter_value.setOnAction(event -> { + // Make a copy so that we can undo the change + final ItemSampleViewFilter filter = new ItemSampleViewFilter(modelItem.getModelItem().getSampleViewFilter()); + filter.setFilterValue(Double.parseDouble(filter_value.getText())); + + new ChangeSampleViewFilterCommand(undo, modelItem.getModelItem(), filter); + }); + + // Todo: move to right side of row + final HBox second_row = new HBox(5, sample_count, new Region(), filter_type, filter_value); + + // Combo should fill the available space. // Tried HBox.setHgrow(items, Priority.ALWAYS) etc., // but always resulted in shrinking the label and button. @@ -104,7 +153,7 @@ public SampleView(final Model model) { sample_count.setPadding(new Insets(5)); sample_table.setPadding(new Insets(0, 5, 5, 5)); VBox.setVgrow(sample_table, Priority.ALWAYS); - getChildren().setAll(top_row, sample_count, sample_table); + getChildren().setAll(top_row, second_row, sample_table); // TODO Add 'export' to sample view? CSV in a format usable by import @@ -113,10 +162,11 @@ public SampleView(final Model model) { private void createSampleTable() { - TableColumn col = new TableColumn<>(Messages.TimeColumn); + TableColumn col = new TableColumn<>(Messages.TimeColumn); final VTypeFormat format = DoubleVTypeFormat.get(); col.setCellValueFactory(cell -> new SimpleStringProperty(TimestampFormats.FULL_FORMAT.format(org.phoebus.core.vtypes.VTypeHelper.getTimestamp(cell.getValue().getVType())))); sample_table.getColumns().add(col); + sample_table.getSortOrder().add(col); col = new TableColumn<>(Messages.ValueColumn); col.setCellValueFactory(cell -> new SimpleStringProperty(format.format(cell.getValue().getVType()))); @@ -131,6 +181,10 @@ private void createSampleTable() { col.setCellValueFactory(cell -> new SimpleStringProperty(VTypeHelper.getMessage(cell.getValue().getVType()))); sample_table.getColumns().add(col); + col = new TableColumn<>(Messages.PVColumn); + col.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().getPVName())); + sample_table.getColumns().add(col); + col = new TableColumn<>(Messages.SampleView_Source); col.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().getSource())); sample_table.getColumns().add(col); @@ -138,67 +192,344 @@ private void createSampleTable() { sample_table.setMaxWidth(Double.MAX_VALUE); sample_table.setPlaceholder(new Label(Messages.SampleView_SelectItem)); sample_table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + sample_table.setItems(sorted_samples); + sorted_samples.comparatorProperty().bind(sample_table.comparatorProperty()); } - private void select(final ModelItem modelItem) { + private void select(final ModelItemListItem modelItem) { this.modelItem = modelItem; - Activator.thread_pool.submit(this::getSamples); + if (modelItem != null && !modelItem.isAllSelection()) { + // Update samples off the UI thread + Activator.thread_pool.submit(this::getSamples); + } else{ + // Update samples off the UI thread + Activator.thread_pool.submit(this::getSamplesAll); + } } - private void update() { - final List model_items = model.getItems(); + public void update() { + final List model_items = model.getItems().stream().map(ModelItemListItem::new).collect(Collectors.toList()); items.getItems().setAll(model_items); - if (modelItem != null) - items.getSelectionModel().select(modelItem); + items.getItems().add(new ModelItemListItem()); // Add an item to the combobox to display all samples from all PVs + + if (modelItem == null) { + return; + } + + items.getSelectionModel().select(modelItem); + filter_value.setText(String.valueOf(this.modelItem.getModelItem().getSampleViewFilter().getFilterValue())); + filter_type.setValue(this.modelItem.getModelItem().getSampleViewFilter().getFilterType()); + // Update samples off the UI thread - Activator.thread_pool.submit(this::getSamples); + if (modelItem.isAllSelection()) { + Activator.thread_pool.submit(this::getSamplesAll); + } else { + Activator.thread_pool.submit(this::getSamples); + } } private void getSamples() { - //final ModelItem item = model.getItem(modelItem); - final ObservableList samples = FXCollections.observableArrayList(); - if (modelItem != null) { - final PlotSamples item_samples = modelItem.getSamples(); + //final ModelItem item = model.getItem(item_name); + final ObservableList samples = FXCollections.observableArrayList(); + if (modelItem != null && !modelItem.isAllSelection()) { + final PlotSamples item_samples = modelItem.getModelItem().getSamples(); try { if (item_samples.getLock().tryLock(2, TimeUnit.SECONDS)) { final int N = item_samples.size(); - for (int i = 0; i < N; ++i) - samples.add(item_samples.get(i)); + for (int i = 0; i < N; ++i) { + PlotSampleWrapper wrapped_sample = new PlotSampleWrapper(item_samples.get(i), modelItem.getModelItem()); + samples.add(wrapped_sample); + } item_samples.getLock().unlock(); } } catch (Exception ex) { - Activator.logger.log(Level.WARNING, "Cannot access samples for " + modelItem.getResolvedName(), ex); + Activator.logger.log(Level.WARNING, "Cannot access samples for " + modelItem.getModelItem().getResolvedName(), ex); + } + } + all_samples_size = samples.size(); + final ObservableList filtered_samples = runSampleViewFilter(samples); + + // Update UI + Platform.runLater(() -> updateSamples(filtered_samples)); + } + + private void getSamplesAll() { + //final List items = model.getItems(); + final List items = this.items.getItems().stream() + .filter(item -> !item.isAllSelection()) + .map(ModelItemListItem::getModelItem) + .collect(Collectors.toList()); + final ObservableList samples = FXCollections.observableArrayList(); + + if (!items.isEmpty()) { + for (ModelItem item : items) { + final PlotSamples item_samples = item.getSamples(); + try { + if (item_samples.getLock().tryLock(2, TimeUnit.SECONDS)) { + final int N = item_samples.size(); + for (int i = 0; i < N; ++i) { + PlotSampleWrapper wrapped_sample = new PlotSampleWrapper(item_samples.get(i), item); + samples.add(wrapped_sample); + } + item_samples.getLock().unlock(); + } + } catch (Exception ex) { + Activator.logger.log(Level.WARNING, "Cannot access samples for " + item.getResolvedName(), ex); + } } } + all_samples_size = samples.size(); + final ObservableList filtered_samples = runSampleViewFilter(samples); + // Update UI - Platform.runLater(() -> updateSamples(samples)); + Platform.runLater(() -> updateSamples(filtered_samples)); + } + + private void updateSamples(ObservableList samples) { + this.samples.setAll(samples); + + // Display the PVitem name (Column 4) + sample_table.getColumns().get(4).setVisible(modelItem != null && modelItem.isAllSelection()); // Hide the PVitem name (Column 4) when not needed + filter_type.setVisible(modelItem != null && !modelItem.isAllSelection()); + filter_value.setVisible(modelItem != null && !modelItem.isAllSelection()); + + // Hide samples that are not visible in the plot when viewing all items + sample_count.setText(Messages.SampleView_Count + " " + all_samples_size + + " (" + Messages.SampleView_Count_Visible + " " + this.samples.size() + ")"); + } + + private ObservableList runSampleViewFilter(ObservableList samples) { + final ObservableList new_samples = FXCollections.observableArrayList(); + + if (samples.isEmpty()) { + return new_samples; + } + + // Store the last viewed sample for each ModelItem, + // so that we can compare the current sample with the last viewed sample of that ModelItem + HashMap last_viewed_sample = new HashMap<>(); + + for (PlotSampleWrapper sample : samples) { + + if (modelItem.isAllSelection() && !sample.getModelItem().isVisible()) continue; + + last_viewed_sample.putIfAbsent(sample.getModelItem(), sample); + PlotSampleWrapper previous_sample_for_item = last_viewed_sample.get(sample.getModelItem()); + + // Enum samples are compared by their index + double sample_value = sample.getSample().getValue(); + double previous_sample_value = previous_sample_for_item.getSample().getValue(); + + + ItemSampleViewFilter.FilterType filter_type = sample.getModelItem().getSampleViewFilter().getFilterType(); + double filter_value = sample.getModelItem().getSampleViewFilter().getFilterValue(); + + Alarm alarm = Alarm.alarmOf(sample.getSample().getVType()); + Alarm previous_alarm = Alarm.alarmOf(previous_sample_for_item.getSample().getVType()); + + switch (filter_type) { + case NO_FILTER: + new_samples.add(sample); + break; + case ALARM_UP: + if (alarm.getSeverity().compareTo(previous_alarm.getSeverity()) > 0) { + new_samples.add(sample); + } + last_viewed_sample.put(sample.getModelItem(), sample); + break; + case ALARM_CHANGES: + if (!alarm.getSeverity().equals(previous_alarm.getSeverity())) { + new_samples.add(sample); + last_viewed_sample.put(sample.getModelItem(), sample); + } + break; + case THRESHOLD_UP: + // Handle sample bundles + if (sample.getVType() instanceof VStatistics) { + double previous_sample_value_max = ((VStatistics) previous_sample_for_item.getVType()).getMax(); + double sample_value_min = ((VStatistics) sample.getVType()).getMin(); + double sample_value_max = ((VStatistics) sample.getVType()).getMax(); + + // Compare maximum of prev to minimum of current. + // also check if threshold was passed within a bundle + if ((previous_sample_value_max <= filter_value && sample_value_min > filter_value) + || (sample_value_min < filter_value && sample_value_max >= filter_value)) { + new_samples.add(sample); + } + last_viewed_sample.put(sample.getModelItem(), sample); + continue; + } + + if (!(sample.getVType() instanceof VNumber || sample.getVType() instanceof VEnum)) { + //System.out.println("Cannot compare non-numerical types"); + new_samples.add(sample); + continue; + } + + if (sample_value >= filter_value && previous_sample_value < filter_value) { + new_samples.add(sample); + } + last_viewed_sample.put(sample.getModelItem(), sample); + break; + case THRESHOLD_DOWN: + // Handle sample bundles + if (sample.getVType() instanceof VStatistics) { + double previous_sample_value_min = ((VStatistics) previous_sample_for_item.getVType()).getMin(); + double sample_value_min = ((VStatistics) sample.getVType()).getMin(); + double sample_value_max = ((VStatistics) sample.getVType()).getMax(); + + // Compare minimum of prev to maximum of current. + // also check if threshold was passed within a bundle + if ((previous_sample_value_min > filter_value && sample_value_max <= filter_value) + || (sample_value_max > filter_value && sample_value_min <= filter_value)) { + new_samples.add(sample); + } + last_viewed_sample.put(sample.getModelItem(), sample); + continue; + } + + if (!(sample.getVType() instanceof VNumber || sample.getVType() instanceof VEnum)) { + //System.out.println("Cannot compare non-numerical types"); + new_samples.add(sample); + continue; + } + + if (previous_sample_value > filter_value && sample_value <= filter_value) { + new_samples.add(sample); + } + last_viewed_sample.put(sample.getModelItem(), sample); + break; + case THRESHOLD_CHANGES: + if (sample.getVType() instanceof VStatistics) { + double previous_sample_value_max = ((VStatistics) previous_sample_for_item.getVType()).getMax(); + double sample_value_min = ((VStatistics) sample.getVType()).getMin(); + double sample_value_max = ((VStatistics) sample.getVType()).getMax(); + + // Compare maximum of prev to minimum of current. + // also check if threshold was passed within a bundle + if ((previous_sample_value_max >= filter_value && sample_value_min < filter_value) + || (previous_sample_value_max <= filter_value && sample_value_min > filter_value) + || (sample_value_min < filter_value && sample_value_max >= filter_value)) { + new_samples.add(sample); + last_viewed_sample.put(sample.getModelItem(), sample); + } + continue; + } + + if (!(sample.getVType() instanceof VNumber || sample.getVType() instanceof VEnum)) { + //System.out.println("Cannot compare non-numerical types"); + new_samples.add(sample); + continue; + } + + if ((sample_value >= filter_value && previous_sample_value < filter_value) + || (sample_value < filter_value && previous_sample_value >= filter_value)) { + new_samples.add(sample); + last_viewed_sample.put(sample.getModelItem(), sample); + } + break; + } + } + return new_samples; } - private void updateSamples(final ObservableList samples) { - sample_count.setText(Messages.SampleView_Count + " " + samples.size()); - sample_table.setItems(samples); + // For also displaying the PVitem name in the list + private static class PlotSampleWrapper { + private final PlotSample sample; + private final ModelItem model_item; + + public PlotSampleWrapper(final PlotSample sample, final ModelItem model_item) { + this.sample = sample; + this.model_item = model_item; + } + + public PlotSample getSample() { + return sample; + } + + public ModelItem getModelItem() { + return model_item; + } + + public VType getVType() { + return sample.getVType(); + } + + public String getSource() { + return sample.getSource(); + } + + public String getPVName() { + return model_item.getResolvedName(); + } + + @Override + public String toString() { + return sample.toString(); + } + } + + // For adding an item to the combobox to display all samples from all PVs + private static class ModelItemListItem { + private final ModelItem model_item; + private final boolean is_all_selection; + + public ModelItemListItem(final ModelItem model_item) { + this.model_item = model_item; + this.is_all_selection = false; + } + + public ModelItemListItem() { + model_item = null; + this.is_all_selection = true; + } + + public ModelItem getModelItem() { + return model_item; + } + + public boolean isAllSelection() { + return is_all_selection; + } + + @Override + public String toString() { + if (is_all_selection) { + return "All Items"; + } else { + return model_item.getResolvedName(); + } + } } /** * Cell factory for the combo box. If user has set a non-empty display name, use it in the item * list together with the resolved PV name. If not, use only resolved PV name. */ - private static class ModelItemListCellFactory implements Callback, ListCell> { + private static class ModelItemListCellFactory implements Callback, ListCell> { @Override - public ListCell call(ListView param) { + public ListCell call(ListView param) { return new ListCell<>() { @Override - protected void updateItem(ModelItem item, boolean empty) { + protected void updateItem(ModelItemListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) { - if (item.getResolvedName().equals(item.getDisplayName()) || item.getDisplayName().isEmpty()) { - setText(item.getResolvedName()); + if (item.isAllSelection()) { + //setText(Messages.SampleView_AllItems); + setText("All Items"); + return; + } + + if (item.getModelItem().getResolvedName().equals(item.getModelItem().getDisplayName()) || item.getModelItem().getDisplayName().isEmpty()) { + //System.out.println("item.getModelItem().getResolvedName(): " + item.getModelItem().getResolvedName()); + setText(item.getModelItem().getResolvedName()); } else { - setText(item.getDisplayName() + " (" + item.getResolvedName() + ")"); + setText(item.getModelItem().getDisplayName() + " (" + item.getModelItem().getResolvedName() + ")"); } } } }; } } -} +} \ No newline at end of file diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/sampleview/ToggleSampleViewPositionMenuItem.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/sampleview/ToggleSampleViewPositionMenuItem.java new file mode 100644 index 0000000000..8853db3bf9 --- /dev/null +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/sampleview/ToggleSampleViewPositionMenuItem.java @@ -0,0 +1,27 @@ +package org.csstudio.trends.databrowser3.ui.sampleview; + + +import javafx.scene.control.MenuItem; +import org.csstudio.trends.databrowser3.Messages; +import org.csstudio.trends.databrowser3.ui.Perspective; +import org.phoebus.ui.javafx.ImageCache; + +/** Menu item to toggle position of SampleView between bottom_tabs and main view + * @author Thomas Lehrach + */ +public class ToggleSampleViewPositionMenuItem extends MenuItem +{ + public ToggleSampleViewPositionMenuItem(final Perspective perspective) + { + if (perspective.isSampleViewInBottomTabs()) { + setText(Messages.SampleView_Move_Up); + setGraphic(ImageCache.getImageView(SampleView.class, "/icons/up.png")); + setOnAction(event -> perspective.setSampleviewLocation(false)); + } + else { + setGraphic(ImageCache.getImageView(SampleView.class, "/icons/down.png")); + setText(Messages.SampleView_Move_Down); + setOnAction(event -> perspective.setSampleviewLocation(true)); + } + } +} diff --git a/app/databrowser/src/main/resources/org/csstudio/trends/databrowser3/messages.properties b/app/databrowser/src/main/resources/org/csstudio/trends/databrowser3/messages.properties index da2e1c39eb..8a70a4a647 100644 --- a/app/databrowser/src/main/resources/org/csstudio/trends/databrowser3/messages.properties +++ b/app/databrowser/src/main/resources/org/csstudio/trends/databrowser3/messages.properties @@ -213,6 +213,7 @@ PointType=Point PointTypeTT=How to mark individual samples of the trace PosErrColumn=Positive Error PropertiesTabName=Properties +PVColumn=PV PVName=PV Name PVUsedInFormulaFmt=The item {0} cannot be deleted because it\nis used as in input to Formula {1}.\nRemove the Formula {1} before deleting {0}. Refresh=Refresh @@ -226,7 +227,19 @@ RequestTypeTT=Archive data request method RequestTypeWarning=Request Type Warning RequestTypeWarningDetail=The 'optimized' request type automatically switches between\noriginal samples and min/max/average information.\nIn rare cases, limitations in this mechanism need to be overcome\nby enforcing 'raw' data retrieval.\nIn most cases, however, requesting 'raw' data will not provide\nany new information but only cause your computer to run out of memory.\n\nAre you sure you need raw data? SampleView_Count=Sample Count: +SampleView_Count_Visible=Visible: +SampleView_FilterChange=Change SampleView Filter +SampleView_FilterTypeTT=Filter Type +SampleView_FilterValueTT=Keep Sample after passing this value SampleView_Item=Item (PV, Formula): +SampleView_ItemFilter_ALARM_CHANGES=Alarm Change +SampleView_ItemFilter_ALARM_UP=Alarm Up +SampleView_ItemFilter_NO_FILTER=None +SampleView_ItemFilter_THRESHOLD_CHANGES=Threshold Passed +SampleView_ItemFilter_THRESHOLD_DOWN=Threshold Passed Down +SampleView_ItemFilter_THRESHOLD_UP=Threshold Passed Up +SampleView_Move_Down=Move SampleView Down +SampleView_Move_Up=Move SampleView Up (Replace Plot) SampleView_Refresh=Refresh SampleView_RefreshTT=Refresh Item list and samples SampleView_SelectItem=- Select item for sample inspection - @@ -271,6 +284,10 @@ TraceLineStyle=Style TraceLineStyleTT=Line Style TraceLineWidth=Width TraceLineWidthTT=Line width, Marker size +TracesFilterType=Filter Type +TracesFilterTypeTT=SampleView filter type +TracesFilterValue=SampleView threshold value +TracesFilterValueTT=Sample shown TraceTableEmpty=No traces TraceType=Trace Type TraceTypeTT=How to display the trace