diff --git a/.github/workflows/build_swagger.yml b/.github/workflows/build_swagger.yml new file mode 100644 index 0000000000..9093c1caf6 --- /dev/null +++ b/.github/workflows/build_swagger.yml @@ -0,0 +1,31 @@ +name: build Swagger documentation + +on: + push: + branches: [ "main", "develop" ] + pull_request: + branches: [ "main", "develop" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Maven and Java Action + uses: s4u/setup-maven-action@v1.13.0 + with: + java-version: '17' + maven-version: '3.9.6' + - name: Get swagger.json + run: | + cd ./services/alarm-logger + mvn spring-boot:run & + export jobpid="$!" + sleep 30 + curl http://localhost:8080/v3/api-docs --output ../../docs/swagger.json + kill "$jobpid" + - name: Archive swagger.json + uses: actions/upload-artifact@v4 + with: + name: swagger.json + path: docs/swagger.json diff --git a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.java b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.java index 9f5c368262..6c34dfc5f3 100644 --- a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.java +++ b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.java @@ -48,7 +48,7 @@ else if(clazz.isAssignableFrom(AdvancedSearchViewController.class)){ }); tab = new DockItem(this, loader.load()); controller = loader.getController(); - tab.setOnClosed(event -> { + tab.addClosedNotification(() -> { controller.shutdown(); }); if (resource != null) { diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java index 8fb3365b46..ee0f0721ae 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java @@ -33,6 +33,7 @@ import org.phoebus.framework.spi.AppInstance; import org.phoebus.framework.util.ResourceParser; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.ui.docking.DockItem; import org.phoebus.ui.docking.DockItemWithInput; import org.phoebus.ui.docking.DockPane; @@ -160,6 +161,10 @@ public DataBrowserInstance(final DataBrowserApp app, final boolean minimal) perspective.getModel().addListener(model_listener); } + public DockItem getDockItem() { + return dock_item; + } + @Override public AppDescriptor getAppDescriptor() { @@ -178,7 +183,7 @@ public void raise() dock_item.select(); } - void loadResource(final URI input) + public void loadResource(final URI input) { // Set input ASAP so that other requests to open this // resource will find this instance and not start 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 d5c1f826fd..deb9463aa5 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 @@ -190,6 +190,7 @@ public class Messages FormulaTabTT, Grid, GridLbl, + HideAll, HideTraceWarning, HideTraceWarningDetail, ImportActionLabelFmt, @@ -269,6 +270,7 @@ public class Messages SelectTrace, SeverityColumn, SeverityStatusFmt, + ShowAll, StartEndDialogBtn, StartEndDialogTT, StartTimeLbl, diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/OpenDataBrowser.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/OpenDataBrowser.java index 092691db3e..c3087d2797 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/OpenDataBrowser.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/OpenDataBrowser.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2024 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,14 +10,15 @@ import org.phoebus.framework.workbench.ApplicationService; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.spi.MenuEntry; +import org.phoebus.ui.spi.ToolbarEntry; import javafx.scene.image.Image; -/** Menu entry for opening data browser +/** Menu and toolbar entry for opening data browser * @author Kay Kasemir */ @SuppressWarnings("nls") -public class OpenDataBrowser implements MenuEntry +public class OpenDataBrowser implements MenuEntry, ToolbarEntry { @Override public String getName() diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/ShowHideAllAction.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/ShowHideAllAction.java new file mode 100644 index 0000000000..86f3aa407c --- /dev/null +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/properties/ShowHideAllAction.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2024 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.csstudio.trends.databrowser3.ui.properties; + +import org.csstudio.trends.databrowser3.Activator; +import org.csstudio.trends.databrowser3.Messages; +import org.csstudio.trends.databrowser3.model.AxisConfig; +import org.csstudio.trends.databrowser3.model.Model; +import org.csstudio.trends.databrowser3.model.ModelItem; +import org.phoebus.ui.undo.UndoableAction; +import org.phoebus.ui.undo.UndoableActionManager; + +import javafx.scene.control.MenuItem; + +/** MenuItem to show or hide all items + * @author Kay Kasemir + */ +@SuppressWarnings("nls") +public class ShowHideAllAction extends MenuItem +{ + private class ShowHideAll extends UndoableAction + { + final private Model model; + final private boolean show; + + ShowHideAll(final UndoableActionManager operations_manager, + final Model model, final boolean show) + { + super(show ? Messages.ShowAll : Messages.HideAll); + this.model = model; + this.show = show; + operations_manager.execute(this); + } + + @Override + public void run() + { + for (ModelItem item : model.getItems()) + item.setVisible(show); + for (AxisConfig axis : model.getAxes()) + axis.setVisible(model.hasAxisActiveItems(axis)); + } + + @Override + public void undo() + { + for (ModelItem item : model.getItems()) + item.setVisible(!show); + for (AxisConfig axis : model.getAxes()) + axis.setVisible(model.hasAxisActiveItems(axis)); + } + } + + /** @param model Model + * @param undo Undo manager + * @param show Show all, or hide all? + */ + public ShowHideAllAction(final Model model, final UndoableActionManager undo, final boolean show) + { + super(show ? Messages.ShowAll : Messages.HideAll, + Activator.getIcon(show ? "checkbox" : "checkbox_unchecked")); + setOnAction(event -> new ShowHideAll(undo, model, show)); + } +} 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 ed8d995ae0..c8915e07fc 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2020 Oak Ridge National Laboratory. + * Copyright (c) 2018-2024 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -733,6 +733,12 @@ private void createContextMenu() items.add(new EditMultipleItemsAction(trace_table, model, undo, selection)); items.add(new SeparatorMenuItem()); + + + items.add(new ShowHideAllAction(model, undo, true)); + items.add(new ShowHideAllAction(model, undo, false)); + + items.add(new SeparatorMenuItem()); // Add PV-based entries final List pvs = selection.stream() diff --git a/app/databrowser/src/main/resources/META-INF/services/org.phoebus.ui.spi.ToolbarEntry b/app/databrowser/src/main/resources/META-INF/services/org.phoebus.ui.spi.ToolbarEntry new file mode 100644 index 0000000000..002c092a64 --- /dev/null +++ b/app/databrowser/src/main/resources/META-INF/services/org.phoebus.ui.spi.ToolbarEntry @@ -0,0 +1 @@ +org.csstudio.trends.databrowser3.OpenDataBrowser 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 f5f6059695..549603f16f 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 @@ -170,6 +170,7 @@ FormulaTabVariable=Variable FormulaTabTT=Double-click input to add to formula, or edit variable name Grid=Grid GridLbl=Grid: +HideAll=Hide All HideTraceWarning=Hide Trace? HideTraceWarningDetail=Hiding a trace can be useful to...\na) temporarily reduce the number of traces on the plot\nb) hide formula input PVs where you are interested in the formula,\n but not the individual inputs\n\nNote however that the Databrowser will still sample data for the hidden trace and request archived data for it so that it's 'ready' when you want to show it again.\n\nIf you don't need this item at all, you should delete it instead of hiding it.\n\nHide trace? ImportActionLabelFmt=Import {0} @@ -249,6 +250,7 @@ SearchTT=Start the channel name search SelectTrace=Select trace to see data sources SeverityColumn=Severity SeverityStatusFmt={0} / {1} +ShowAll=Show All StartEndDialogBtn=... StartEndDialogTT=Open start/end time dialog box StartTimeLbl=Start Time: diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogObjectMappers.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogObjectMappers.java index 0e07d9f6fe..618ae71dae 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogObjectMappers.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogObjectMappers.java @@ -112,6 +112,7 @@ public OlogAttachment deserialize(JsonParser jp, DeserializationContext ctxt) String fileMetadataDescription = node.get("fileMetadataDescription").asText(); OlogAttachment a = new OlogAttachment(); a.setFileName(filename); + a.setId(id); a.setContentType(fileMetadataDescription); return a; } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalender.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalender.java index 8b3c2384ca..0e0aa830d5 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalender.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalender.java @@ -77,9 +77,7 @@ else if(clazz.isAssignableFrom(AdvancedSearchViewController.class)){ log.log(Level.SEVERE, "Failed to acquire a valid logbook client"); } tab = new DockItem(this, loader.getRoot()); - tab.setOnClosed(event -> { - controller.shutdown(); - }); + tab.addClosedNotification(()-> controller.shutdown()); DockPane.getActiveDockPane().addTab(tab); } catch (IOException e) { diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java index a259a46ee8..358ba7fa39 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java @@ -23,16 +23,22 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToolBar; +import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; import org.phoebus.olog.es.api.model.LogGroupProperty; import org.phoebus.olog.es.api.model.OlogLog; +import org.phoebus.ui.javafx.ImageCache; public class LogEntryDisplayController { @@ -52,12 +58,23 @@ public class LogEntryDisplayController { @FXML private Button replyButton; @FXML + private Region spring; + @FXML + private Button goBackButton; + @FXML + private Button goForwardButton; + @FXML private BorderPane emptyPane; @FXML private Node singleLogEntryDisplay; @FXML private Node mergedLogEntryDisplay; + ImageView goBackButtonIcon = ImageCache.getImageView(LogEntryDisplayController.class, "/icons/backward_nav.png"); + ImageView goBackButtonIconDisabled = ImageCache.getImageView(LogEntryDisplayController.class, "/icons/backward_disabled.png"); + ImageView goForwardButtonIcon = ImageCache.getImageView(LogEntryDisplayController.class, "/icons/forward_nav.png"); + ImageView goForwardButtonIconDisabled = ImageCache.getImageView(LogEntryDisplayController.class, "/icons/forward_disabled.png"); + private final SimpleObjectProperty logEntryProperty = new SimpleObjectProperty<>(); @@ -82,6 +99,19 @@ public void initialize() { .bind(Bindings.createBooleanBinding(() -> currentViewProperty.get() == SINGLE, currentViewProperty)); mergedLogEntryDisplay.visibleProperty() .bind(Bindings.createBooleanBinding(() -> currentViewProperty.get() == MERGED, currentViewProperty)); + HBox.setHgrow(spring, Priority.ALWAYS); // Spring to make subsequent elements right-aligned in the toolbar. + + { + ChangeListener goBackButtonDisabledPropertyChangeListener = (property, oldValue, newValue) -> goBackButton.setGraphic(newValue ? goBackButtonIconDisabled : goBackButtonIcon); + goBackButton.disableProperty().addListener(goBackButtonDisabledPropertyChangeListener); + goBackButtonDisabledPropertyChangeListener.changed(goBackButton.disableProperty(), false, true); + } + + { + ChangeListener goForwardButtonDisabledPropertyChangeListener = (property, oldValue, newValue) -> goForwardButton.setGraphic(newValue ? goForwardButtonIconDisabled : goForwardButtonIcon); + goForwardButton.disableProperty().addListener(goForwardButtonDisabledPropertyChangeListener); + goForwardButtonDisabledPropertyChangeListener.changed(goForwardButton.disableProperty(), false, true); + } } @FXML @@ -116,6 +146,20 @@ public void newLogEntry(){ new LogEntryEditorStage(new OlogLog(), null, null).show(); } + @FXML + public void goBack() { + if (logEntryTableViewController.goBackAndGoForwardActions.isPresent()) { + logEntryTableViewController.goBackAndGoForwardActions.get().goBack(); + } + } + + @FXML + public void goForward() { + if (logEntryTableViewController.goBackAndGoForwardActions.isPresent()) { + logEntryTableViewController.goBackAndGoForwardActions.get().goForward(); + } + } + public void setLogEntry(LogEntry logEntry) { if(logEntry == null){ currentViewProperty.set(EMPTY); @@ -147,5 +191,9 @@ public void updateLogEntry(LogEntry logEntry){ public void setLogEntryTableViewController(LogEntryTableViewController logEntryTableViewController){ this.logEntryTableViewController = logEntryTableViewController; + if (logEntryTableViewController.goBackAndGoForwardActions.isPresent()) { + goBackButton.disableProperty().bind(Bindings.isEmpty(logEntryTableViewController.goBackAndGoForwardActions.get().goBackActions)); + goForwardButton.disableProperty().bind(Bindings.isEmpty(logEntryTableViewController.goBackAndGoForwardActions.get().goForwardActions)); + } } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java index 17f20a5a3c..6993b63859 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java @@ -1,5 +1,7 @@ package org.phoebus.logbook.olog.ui; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXMLLoader; import javafx.scene.control.Alert; import org.phoebus.framework.nls.NLS; @@ -29,8 +31,11 @@ public class LogEntryTable implements AppInstance { private final LogEntryTableApp app; private LogEntryTableViewController controller; + public GoBackAndGoForwardActions goBackAndGoForwardActions; + public LogEntryTable(final LogEntryTableApp app) { this.app = app; + goBackAndGoForwardActions = new GoBackAndGoForwardActions(); try { OlogQueryManager ologQueryManager = OlogQueryManager.getInstance(); SearchParameters searchParameters = new SearchParameters(); @@ -44,13 +49,16 @@ public LogEntryTable(final LogEntryTableApp app) { try { if (app.getClient() != null) { if (clazz.isAssignableFrom(LogEntryTableViewController.class)) { - return clazz.getConstructor(LogClient.class, OlogQueryManager.class, SearchParameters.class) - .newInstance(app.getClient(), ologQueryManager, searchParameters); + LogEntryTableViewController logEntryTableViewController = (LogEntryTableViewController) clazz.getConstructor(LogClient.class, OlogQueryManager.class, SearchParameters.class).newInstance(app.getClient(), ologQueryManager, searchParameters); + logEntryTableViewController.setGoBackAndGoForwardActions(goBackAndGoForwardActions); + return logEntryTableViewController; } else if (clazz.isAssignableFrom(AdvancedSearchViewController.class)) { return clazz.getConstructor(LogClient.class, SearchParameters.class) .newInstance(app.getClient(), searchParameters); } else if (clazz.isAssignableFrom(SingleLogEntryDisplayController.class)) { - return clazz.getConstructor(LogClient.class).newInstance(app.getClient()); + SingleLogEntryDisplayController singleLogEntryDisplayController = (SingleLogEntryDisplayController) clazz.getConstructor(LogClient.class).newInstance(app.getClient()); + singleLogEntryDisplayController.setSelectLogEntryInUI(id -> goBackAndGoForwardActions.loadLogEntryWithID(id)); + return singleLogEntryDisplayController; } else if (clazz.isAssignableFrom(LogEntryDisplayController.class)) { return clazz.getConstructor().newInstance(); } else if (clazz.isAssignableFrom(LogPropertiesController.class)) { @@ -80,7 +88,7 @@ public LogEntryTable(final LogEntryTableApp app) { loader.load(); controller = loader.getController(); DockItem tab = new DockItem(this, loader.getRoot()); - tab.setOnClosed(event -> controller.shutdown()); + tab.addClosedNotification(()-> controller.shutdown()); DockPane.getActiveDockPane().addTab(tab); } catch (IOException e) { log.log(Level.WARNING, "Cannot load UI", e); @@ -118,4 +126,76 @@ public void save(final Memento memento) { public void logEntryChanged(LogEntry logEntry){ controller.logEntryChanged(logEntry); } + + protected class GoBackAndGoForwardActions { + + private GoBackAndGoForwardActions() { + goBackActions = FXCollections.observableArrayList(); + goForwardActions = FXCollections.observableArrayList(); + } + + protected ObservableList goBackActions; + protected ObservableList goForwardActions; + + private boolean isRecordingHistoryDisabled = false; // Used to not add go-back actions when clicking "back". + + protected boolean getIsRecordingHistoryDisabled() { + return isRecordingHistoryDisabled; + } + public void setIsRecordingHistoryDisabled(boolean isRecordingHistoryDisabled) { + this.isRecordingHistoryDisabled = isRecordingHistoryDisabled; + } + + private void gotoLogEntry(LogEntry logEntry) { + isRecordingHistoryDisabled = true; + boolean selected = controller.selectLogEntry(logEntry); + if (!selected) { + // The log entry was not available in the TreeView. Set the log entry without selecting it in the treeview: + controller.setLogEntry(logEntry); + } + isRecordingHistoryDisabled = false; + } + + protected void addGoBackAction() { + LogEntry currentLogEntry = controller.getLogEntry(); + + if (currentLogEntry != null) { + goBackActions.add(0, () -> gotoLogEntry(currentLogEntry)); + } + } + + private void addGoForwardAction() { + LogEntry currentLogEntry = controller.getLogEntry(); + + if (currentLogEntry != null) { + goForwardActions.add(0, () -> gotoLogEntry(currentLogEntry)); + } + } + + private void loadLogEntryWithID(Long id) { + goForwardActions.clear(); + addGoBackAction(); + + LogEntry logEntry = controller.client.getLog(id); + gotoLogEntry(logEntry); + } + + protected void goBack() { + if (goBackActions.size() > 0) { + addGoForwardAction(); + Runnable goBackAction = goBackActions.get(0); + goBackActions.remove(0); + goBackAction.run(); + } + } + + protected void goForward() { + if (goForwardActions.size() > 0) { + addGoBackAction(); + Runnable goForwardAction = goForwardActions.get(0); + goForwardActions.remove(0); + goForwardAction.run(); + } + } + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 9cb6a534f8..0fe7b1b4cc 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -105,12 +105,18 @@ public class LogEntryTableViewController extends LogbookSearchController { * * @param logClient Log client implementation */ - public LogEntryTableViewController(LogClient logClient, OlogQueryManager ologQueryManager, SearchParameters searchParameters) { + public LogEntryTableViewController(LogClient logClient, + OlogQueryManager ologQueryManager, + SearchParameters searchParameters) { setClient(logClient); this.ologQueryManager = ologQueryManager; this.searchParameters = searchParameters; } + protected void setGoBackAndGoForwardActions(LogEntryTable.GoBackAndGoForwardActions goBackAndGoForwardActions) { + this.goBackAndGoForwardActions = Optional.of(goBackAndGoForwardActions); + } + private final SimpleIntegerProperty hitCountProperty = new SimpleIntegerProperty(0); private final SimpleIntegerProperty pageSizeProperty = new SimpleIntegerProperty(LogbookUIPreferences.search_result_page_size); @@ -121,6 +127,7 @@ public LogEntryTableViewController(LogClient logClient, OlogQueryManager ologQue private final SearchParameters searchParameters; + protected Optional goBackAndGoForwardActions = Optional.empty(); @FXML public void initialize() { @@ -182,6 +189,10 @@ public void initialize() { tableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { // Update detailed view, but only if selection contains a single item. if (newValue != null && tableView.getSelectionModel().getSelectedItems().size() == 1) { + if (goBackAndGoForwardActions.isPresent() && !goBackAndGoForwardActions.get().getIsRecordingHistoryDisabled()) { + goBackAndGoForwardActions.get().addGoBackAction(); + goBackAndGoForwardActions.get().goForwardActions.clear(); + } logEntryDisplayController.setLogEntry(newValue.getLogEntry()); } List logEntries = tableView.getSelectionModel().getSelectedItems() @@ -392,7 +403,16 @@ private void refresh() { for (TableViewListItem selectedItem : selectedLogEntries) { for (TableViewListItem item : tableView.getItems()) { if (item.getLogEntry().getId().equals(selectedItem.getLogEntry().getId())) { - Platform.runLater(() -> tableView.getSelectionModel().select(item)); + Platform.runLater(() -> { + if (goBackAndGoForwardActions.isPresent()) { + goBackAndGoForwardActions.get().setIsRecordingHistoryDisabled(true); // Do not create a "Back" action for the automatic reload. + tableView.getSelectionModel().select(item); + goBackAndGoForwardActions.get().setIsRecordingHistoryDisabled(false); + } + else { + tableView.getSelectionModel().select(item); + } + }); } } } @@ -530,6 +550,14 @@ public void logEntryChanged(LogEntry logEntry) { logEntryDisplayController.updateLogEntry(logEntry); } + protected LogEntry getLogEntry() { + return logEntryDisplayController.getLogEntry(); + } + + protected void setLogEntry(LogEntry logEntry) { + logEntryDisplayController.setLogEntry(logEntry); + } + /** * Selects a log entry as a result of an action outside the {@link TreeView}, but selection happens on the * {@link TreeView} item, if it exists (match on log entry id). If it does not exist, selection is cleared diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java index 271cf5859f..ac04bfbd4e 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java @@ -19,6 +19,7 @@ public class Messages ArchivedLaunchExternalAppFailed, ArchivedNoEntriesFound, ArchivedSaveFailed, + Back, CloseRequestHeader, CloseRequestButtonContinue, CloseRequestButtonDiscard, @@ -29,6 +30,7 @@ public class Messages FileSave, FileSaveFailed, FileTooLarge, + Forward, GroupingFailed, GroupSelectedEntries, Level, diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java index ba6960d68f..997a04905e 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java @@ -30,6 +30,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -87,11 +89,17 @@ public class SingleLogEntryDisplayController extends HtmlAwareController { private final SimpleBooleanProperty logEntryUpdated = new SimpleBooleanProperty(); + private Optional> selectLogEntryInUI = Optional.empty(); + public SingleLogEntryDisplayController(LogClient logClient) { super(logClient.getServiceUrl()); this.logClient = logClient; } + public void setSelectLogEntryInUI(Consumer selectLogEntryInUI) { + this.selectLogEntryInUI = Optional.of(id -> selectLogEntryInUI.accept(id)); + }; + @FXML public void initialize() { @@ -101,9 +109,12 @@ public void initialize() { copyURLButton.visibleProperty().setValue(LogbookUIPreferences.web_client_root_URL != null && !LogbookUIPreferences.web_client_root_URL.isEmpty()); - webEngine = webView.getEngine(); - // This will make links clicked in the WebView to open in default browser. - webEngine.getLoadWorker().stateProperty().addListener(new HyperLinkRedirectListener(webView)); + { + Optional webClientRoot = LogbookUIPreferences.web_client_root_URL == null || LogbookUIPreferences.web_client_root_URL.equals("") ? Optional.empty() : Optional.of(LogbookUIPreferences.web_client_root_URL); + webEngine = webView.getEngine(); + // This will make links clicked in the WebView to open in default browser. + webEngine.getLoadWorker().stateProperty().addListener(new HyperLinkRedirectListener(webView, webClientRoot, selectLogEntryInUI)); + } updatedIndicator.visibleProperty().bind(logEntryUpdated); updatedIndicator.setOnMouseEntered(me -> updatedIndicator.setCursor(Cursor.HAND)); @@ -199,11 +210,21 @@ protected void finalize() { fileAttachment.setContentType(attachment.getContentType()); fileAttachment.setThumbnail(false); fileAttachment.setFileName(attachment.getName()); + // A bit of a hack here. The idea is to create a temporary file with a known name, + // i.e. without the random file name part. + // Files.createdTempFile does not support it, so a bit of workaround is needed. try { - Path temp = Files.createTempFile("phoebus", attachment.getName()); - Files.copy(logClient.getAttachment(logEntry.getId(), attachment.getName()), temp, StandardCopyOption.REPLACE_EXISTING); - fileAttachment.setFile(temp.toFile()); - temp.toFile().deleteOnExit(); + // This creates a temp file with a random part + Path random = Files.createTempFile(attachment.getId(), attachment.getName()); + // This does NOT create a file + Path nonRandom = random.resolveSibling(attachment.getId()); + if(!Files.exists(nonRandom.toAbsolutePath())){ + // Moves the temp file with random part to file with non-random part. + nonRandom = Files.move(random, nonRandom); + Files.copy(logClient.getAttachment(logEntry.getId(), attachment.getName()), nonRandom, StandardCopyOption.REPLACE_EXISTING); + fileAttachment.setFile(nonRandom.toFile()); + nonRandom.toFile().deleteOnExit(); + } } catch (LogbookException | IOException e) { Logger.getLogger(SingleLogEntryDisplayController.class.getName()) .log(Level.WARNING, "Failed to retrieve attachment " + fileAttachment.getFileName(), e); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java index be2d7b9c9e..dfb9c0727a 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java @@ -273,7 +273,15 @@ public void initialize() { titleProperty.set(logEntry.getTitle()); textArea.textProperty().bindBidirectional(descriptionProperty); - descriptionProperty.set(logEntry.getDescription() != null ? logEntry.getDescription() : ""); + if (logEntry.getSource() != null) { + descriptionProperty.set(logEntry.getSource()); + } + else if (logEntry.getDescription() != null) { + descriptionProperty.set(logEntry.getDescription()); + } + else { + descriptionProperty.set(""); + } descriptionProperty.addListener((observable, oldValue, newValue) -> isDirty = true); Image tagIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/add_tag.png"); diff --git a/app/logbook/olog/ui/src/main/resources/icons/backward_disabled.png b/app/logbook/olog/ui/src/main/resources/icons/backward_disabled.png new file mode 100644 index 0000000000..e5d50acfec Binary files /dev/null and b/app/logbook/olog/ui/src/main/resources/icons/backward_disabled.png differ diff --git a/app/logbook/olog/ui/src/main/resources/icons/backward_nav.png b/app/logbook/olog/ui/src/main/resources/icons/backward_nav.png new file mode 100644 index 0000000000..84ea07d5b8 Binary files /dev/null and b/app/logbook/olog/ui/src/main/resources/icons/backward_nav.png differ diff --git a/app/logbook/olog/ui/src/main/resources/icons/forward_disabled.png b/app/logbook/olog/ui/src/main/resources/icons/forward_disabled.png new file mode 100644 index 0000000000..ab531ed433 Binary files /dev/null and b/app/logbook/olog/ui/src/main/resources/icons/forward_disabled.png differ diff --git a/app/logbook/olog/ui/src/main/resources/icons/forward_nav.png b/app/logbook/olog/ui/src/main/resources/icons/forward_nav.png new file mode 100644 index 0000000000..2d09a77c24 Binary files /dev/null and b/app/logbook/olog/ui/src/main/resources/icons/forward_nav.png differ diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryDisplay.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryDisplay.fxml index 6f84534947..2ed4d85964 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryDisplay.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryDisplay.fxml @@ -32,6 +32,11 @@ - - - + diff --git a/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties b/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties index 35c193d8b3..90861c1254 100644 --- a/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties +++ b/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties @@ -17,4 +17,7 @@ default_search_query=tags=golden # If declared add a date automatically in the name of the snapshot "Take Snapshot" #default_snapshot_name_date_format=yyyy-MM-dd HH:mm:ss -this.is.a.test=MUSIGNY \ No newline at end of file +# Defines the default snapshot mode +# READ_PVS: the classic mode where PV values are read from IOCs +# FROM_ARCHIVER: PV values read from archiver at point in time selected by user +default_snapshot_mode=READ_PVS \ No newline at end of file diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotMode.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotMode.java new file mode 100644 index 0000000000..d77c157a03 --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotMode.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.model; + +public enum SnapshotMode { + + READ_PVS("Read PVssss"), + READ_FROM_ARCHIVER("Read from Archiver"); + + SnapshotMode(String name){ + this.name = name; + } + + private String name; +} diff --git a/core/framework/src/main/java/org/phoebus/framework/preferences/PropertyPreferenceLoader.java b/core/framework/src/main/java/org/phoebus/framework/preferences/PropertyPreferenceLoader.java index 92917baa71..74e63822f4 100644 --- a/core/framework/src/main/java/org/phoebus/framework/preferences/PropertyPreferenceLoader.java +++ b/core/framework/src/main/java/org/phoebus/framework/preferences/PropertyPreferenceLoader.java @@ -18,6 +18,8 @@ import java.util.Properties; import java.util.prefs.Preferences; +import org.phoebus.framework.workbench.Locations; + /** Load preferences from a property file * @author Kay Kasemir */ @@ -46,7 +48,16 @@ public static void load(final InputStream stream) throws Exception final String pack = "/" + prop.substring(0, sep).replace('.', '/'); final String name = prop.substring(sep+1); - final String value = props.getProperty(prop); + String value = props.getProperty(prop); + + if (value.contains("$(phoebus.install)")) + value = value.replace("$(phoebus.install)", Locations.install().toString()).replace("\\", "/").replace(" ", "%20"); + if (value.contains("$(phoebus.user)")) + value = value.replace("$(phoebus.user)", Locations.user().toString()).replace("\\", "/").replace(" ", "%20"); + if (value.contains("$(user.home)")) + value = value.replace("$(user.home)", System.getProperty("user.home").toString()).replace("\\", "/").replace(" ", "%20"); + + final Preferences prefs = Preferences.userRoot().node(pack); prefs.put(name, value); // System.out.println(pack + "/" + name + "=" + value); diff --git a/core/launcher/src/main/java/org/phoebus/product/LaunchErrorDialog.java b/core/launcher/src/main/java/org/phoebus/product/LaunchErrorDialog.java new file mode 100644 index 0000000000..8c4b05f668 --- /dev/null +++ b/core/launcher/src/main/java/org/phoebus/product/LaunchErrorDialog.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.product; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Minimal Swing dialog showing an error message as extracted from + * an {@link Exception}. + */ +public class LaunchErrorDialog extends JFrame { + + /** + * Access method. + * @param exception The {@link Exception} from which to + * extract text shown in the dialog. + */ + public static void show(Exception exception){ + new LaunchErrorDialog(exception).setVisible(true); + } + + private LaunchErrorDialog(Exception exception){ + setSize(600, 300); + setTitle(Messages.launchErrorTitle); + String message = exception.getMessage(); + // If there is no exception message, use stack track instead + if(message == null) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + exception.printStackTrace(printWriter); + message = stringWriter.toString(); + } + JTextArea jTextArea = new JTextArea(message); + jTextArea.setBorder(new EmptyBorder(10, 10, 10, 10)); + jTextArea.setLineWrap(true); + add(jTextArea, BorderLayout.CENTER); + // Exit when the JFrame is closed + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + // Center on screen + setLocationRelativeTo(null); + setVisible(true); + } +} diff --git a/core/launcher/src/main/java/org/phoebus/product/Launcher.java b/core/launcher/src/main/java/org/phoebus/product/Launcher.java index 481c9934a7..5e5d358a3a 100644 --- a/core/launcher/src/main/java/org/phoebus/product/Launcher.java +++ b/core/launcher/src/main/java/org/phoebus/product/Launcher.java @@ -1,5 +1,14 @@ package org.phoebus.product; +import javafx.application.Application; +import org.phoebus.framework.preferences.PropertyPreferenceLoader; +import org.phoebus.framework.spi.AppDescriptor; +import org.phoebus.framework.spi.AppResourceDescriptor; +import org.phoebus.framework.workbench.ApplicationService; +import org.phoebus.framework.workbench.Locations; +import org.phoebus.ui.application.ApplicationServer; +import org.phoebus.ui.application.PhoebusApplication; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -15,24 +24,14 @@ import java.util.prefs.Preferences; import java.util.stream.Collectors; -import org.phoebus.framework.preferences.PropertyPreferenceLoader; -import org.phoebus.framework.spi.AppDescriptor; -import org.phoebus.framework.spi.AppResourceDescriptor; -import org.phoebus.framework.workbench.ApplicationService; -import org.phoebus.framework.workbench.Locations; -import org.phoebus.ui.application.ApplicationServer; -import org.phoebus.ui.application.PhoebusApplication; - -import javafx.application.Application; - @SuppressWarnings("nls") -public class Launcher -{ - public static void main(final String[] original_args) throws Exception - { +public class Launcher { + public static void main(final String[] original_args) throws Exception { LogManager.getLogManager().readConfiguration(Launcher.class.getResourceAsStream("/logging.properties")); final Logger logger = Logger.getLogger(Launcher.class.getName()); + boolean showLaunchError = false; + // Can't change default charset, but warn if it's not UTF-8. // Config files for displays, data browser etc. explicitly use XMLUtil.ENCODING = "UTF-8". // EPICS database files, strings in Channel Access or PVAccess are expected to use UTF-8. @@ -40,11 +39,10 @@ public static void main(final String[] original_args) throws Exception // but library code including JCA simply calls new String(byte[]). // The underlying Charset.defaultCharset() checks "file.encoding", // but this happens at an early stage of VM startup. - // Calling System.setPropertu("file.encoding", "UTF-8") in main() is already too late, + // Calling System.setProperty("file.encoding", "UTF-8") in main() is already too late, // must add -D"file.encoding=UTF-8" to java start up or JAVA_TOOL_OPTIONS. final Charset cs = Charset.defaultCharset(); - if (! "UTF-8".equalsIgnoreCase(cs.displayName())) - { + if (!"UTF-8".equalsIgnoreCase(cs.displayName())) { logger.severe("Default charset is " + cs.displayName() + " instead of UTF-8."); logger.severe("Add -D\"file.encoding=UTF-8\" to java command line or JAVA_TOOL_OPTIONS"); } @@ -52,8 +50,7 @@ public static void main(final String[] original_args) throws Exception // Check for site-specific settings.ini bundled into distribution // before potentially adding command-line settings. final File site_settings = new File(Locations.install(), "settings.ini"); - if (site_settings.canRead()) - { + if (site_settings.canRead()) { logger.log(Level.CONFIG, "Loading settings from " + site_settings); PropertyPreferenceLoader.load(new FileInputStream(site_settings)); } @@ -62,41 +59,31 @@ public static void main(final String[] original_args) throws Exception final List args = new ArrayList<>(List.of(original_args)); final Iterator iter = args.iterator(); int port = -1; - try - { - while (iter.hasNext()) - { + try { + while (iter.hasNext()) { final String cmd = iter.next(); - if (cmd.startsWith("-h")) - { + if (cmd.startsWith("-h")) { help(); return; } - if (cmd.equals("-splash")) - { + if (cmd.equals("-splash")) { iter.remove(); Preferences.userNodeForPackage(org.phoebus.ui.Preferences.class) - .putBoolean(org.phoebus.ui.Preferences.SPLASH, true); - } - else if (cmd.equals("-nosplash")) - { + .putBoolean(org.phoebus.ui.Preferences.SPLASH, true); + } else if (cmd.equals("-nosplash")) { iter.remove(); Preferences.userNodeForPackage(org.phoebus.ui.Preferences.class) - .putBoolean(org.phoebus.ui.Preferences.SPLASH, false); - } - else if (cmd.equals("-logging")) - { - if (! iter.hasNext()) + .putBoolean(org.phoebus.ui.Preferences.SPLASH, false); + } else if (cmd.equals("-logging")) { + if (!iter.hasNext()) throw new Exception("Missing -logging file name"); iter.remove(); final String filename = iter.next(); iter.remove(); LogManager.getLogManager().readConfiguration(new FileInputStream(filename)); - } - else if (cmd.equals("-settings")) - { - if (! iter.hasNext()) + } else if (cmd.equals("-settings")) { + if (!iter.hasNext()) throw new Exception("Missing -settings file name"); iter.remove(); final String location = iter.next(); @@ -107,11 +94,9 @@ else if (cmd.equals("-settings")) Preferences.importPreferences(new FileInputStream(location)); else PropertyPreferenceLoader.load(location); - - } - else if (cmd.equals("-export_settings")) - { - if (! iter.hasNext()) + + } else if (cmd.equals("-export_settings")) { + if (!iter.hasNext()) throw new Exception("Missing -export_settings file name"); iter.remove(); final String filename = iter.next(); @@ -119,39 +104,29 @@ else if (cmd.equals("-export_settings")) System.out.println("Exporting settings to " + filename); Preferences.userRoot().node("org/phoebus").exportSubtree(new FileOutputStream(filename)); return; - } - else if (cmd.equals("-server")) - { - if (! iter.hasNext()) + } else if (cmd.equals("-server")) { + if (!iter.hasNext()) throw new Exception("Missing -server port"); iter.remove(); port = Integer.parseInt(iter.next()); iter.remove(); - } - else if (cmd.equals("-list")) - { + } else if (cmd.equals("-list")) { iter.remove(); final Collection apps = ApplicationService.getApplications(); System.out.format("Name Description File Extensions\n"); - for (AppDescriptor app : apps) - { - if (app instanceof AppResourceDescriptor) - { - final AppResourceDescriptor app_res = (AppResourceDescriptor) app; + for (AppDescriptor app : apps) { + if (app instanceof AppResourceDescriptor app_res) { System.out.format("%-20s %-20s %s\n", - "'" + app.getName() + "'", - app.getDisplayName(), - app_res.supportedFileExtentions().stream().collect(Collectors.joining(", "))); - } - else + "'" + app.getName() + "'", + app.getDisplayName(), + app_res.supportedFileExtentions().stream().collect(Collectors.joining(", "))); + } else System.out.format("%-20s %s\n", "'" + app.getName() + "'", app.getDisplayName()); } return; - } - else if (cmd.equals("-main")) - { + } else if (cmd.equals("-main")) { iter.remove(); - if (! iter.hasNext()) + if (!iter.hasNext()) throw new Exception("Missing -main name"); final String main = iter.next(); iter.remove(); @@ -162,16 +137,19 @@ else if (cmd.equals("-main")) // Collect remaining arguments final List new_args = new ArrayList<>(); iter.forEachRemaining(new_args::add); - main_method.invoke(null, new Object[] { new_args.toArray(new String[new_args.size()]) }); + main_method.invoke(null, new Object[]{new_args.toArray(new String[new_args.size()])}); return; + } else if (cmd.equals("-launch_error_dialog")) { + showLaunchError = true; } } - } - catch (Exception ex) - { + } catch (Exception ex) { help(); System.out.println(); ex.printStackTrace(); + if (showLaunchError) { + LaunchErrorDialog.show(ex); + } return; } @@ -180,11 +158,9 @@ else if (cmd.equals("-main")) // Check for an existing instance // If found, pass remaining arguments to it, // instead of starting a new application - if (port > 0) - { + if (port > 0) { final ApplicationServer server = ApplicationServer.create(port); - if (! server.isServer()) - { + if (!server.isServer()) { server.sendArguments(args); return; } @@ -194,8 +170,7 @@ else if (cmd.equals("-main")) Application.launch(PhoebusApplication.class, args.toArray(new String[args.size()])); } - private static void help() - { + private static void help() { System.out.println(" _______ _______ _______ ______ _______ "); System.out.println("( ____ )|\\ /|( ___ )( ____ \\( ___ \\ |\\ /|( ____ \\"); System.out.println("| ( )|| ) ( || ( ) || ( \\/| ( ) )| ) ( || ( \\/"); @@ -210,6 +185,7 @@ private static void help() System.out.println("-help - This text"); System.out.println("-splash - Show splash screen"); System.out.println("-nosplash - Suppress the splash screen"); + System.out.println("-launch_error_dialog - Shows dialog if launch fails. Must be first program argument."); System.out.println("-settings settings.xml - Import settings from file, either exported XML or property file format"); System.out.println("-export_settings settings.xml - Export settings to file"); System.out.println("-logging logging.properties - Load log settings"); @@ -238,5 +214,6 @@ private static void help() System.out.println("-resource '...&target=window' - Opens resource in separate window."); System.out.println("-resource '...&target=window@800x600+200+150' - Opens resource in separate window sized 800 by 600 at x=200, y=150."); System.out.println("-resource '...&target=name_of_pane' - Opens resource in named pane."); + } } diff --git a/core/launcher/src/main/java/org/phoebus/product/Messages.java b/core/launcher/src/main/java/org/phoebus/product/Messages.java new file mode 100644 index 0000000000..00378935e8 --- /dev/null +++ b/core/launcher/src/main/java/org/phoebus/product/Messages.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.product; + +import org.phoebus.framework.nls.NLS; + +public class Messages { + + static + { + // initialize resource bundle + NLS.initializeMessages(Messages.class); + } + + private Messages() + { + } + + public static String launchErrorTitle; + +} diff --git a/core/launcher/src/main/resources/org/phoebus/product/messages.properties b/core/launcher/src/main/resources/org/phoebus/product/messages.properties new file mode 100644 index 0000000000..1f0e15ab40 --- /dev/null +++ b/core/launcher/src/main/resources/org/phoebus/product/messages.properties @@ -0,0 +1,5 @@ +# +# Copyright (C) 2024 European Spallation Source ERIC. +# + +launchErrorTitle=Phoebus Launch Failed \ No newline at end of file diff --git a/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java b/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java index 4eeae71e63..de2a5a3c02 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java @@ -470,6 +470,10 @@ private void backgroundStartup(final JobMonitor monitor, final Splash splash) th }); } + public ToolBar getToolbar() { + return toolbar; + } + private void startUI(final MementoTree memento, final JobMonitor monitor) throws Exception { monitor.beginTask(Messages.MonitorTaskUi, 4); diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java index b6926e4d8b..cca55779cc 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java @@ -22,10 +22,16 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import javafx.stage.Window; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.application.Messages; -import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.javafx.Styles; @@ -36,13 +42,6 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.ContextMenuEvent; @@ -295,14 +294,15 @@ private void showContextMenu(final ContextMenuEvent event) } /** @param dock_parent {@link BorderPane}, {@link SplitDock} or null */ - void setDockParent(final Parent dock_parent) + public void setDockParent(final Parent dock_parent) { if (dock_parent == null || dock_parent instanceof BorderPane || - dock_parent instanceof SplitDock) + dock_parent instanceof SplitDock || + dock_parent instanceof SplitPane) // "dock_parent instanceof SplitPane" is for the case of the Navigator application running this.dock_parent = dock_parent; else - throw new IllegalArgumentException("Expect BorderPane or SplitDock, got " + dock_parent); + throw new IllegalArgumentException("Expected BorderPane or SplitDock or SplitPane, got " + dock_parent); } /** @param name Name, may not be null */ @@ -625,7 +625,22 @@ private void handleDrop(final DragEvent event) public SplitDock split(final boolean horizontally) { final SplitDock split; - if (dock_parent instanceof BorderPane) + + if (dock_parent instanceof SplitDock) + { + final SplitDock parent = (SplitDock) dock_parent; + // Remove this dock pane from parent + final boolean first = parent.removeItem(this); + // Place in split alongside a new dock pane + final DockPane new_pane = new DockPane(); + dockPaneEmptyListeners.stream().forEach(new_pane::addDockPaneEmptyListener); + split = new SplitDock(parent, horizontally, this, new_pane); + setDockParent(split); + new_pane.setDockParent(split); + // Place that new split in the border pane + parent.addItem(first, split); + } + else if (dock_parent instanceof BorderPane) { final BorderPane parent = (BorderPane) dock_parent; // Remove this dock pane from BorderPane @@ -639,11 +654,12 @@ public SplitDock split(final boolean horizontally) // Place that new split in the border pane parent.setCenter(split); } - else if (dock_parent instanceof SplitDock) + else if (dock_parent instanceof SplitPane) { - final SplitDock parent = (SplitDock) dock_parent; - // Remove this dock pane from parent - final boolean first = parent.removeItem(this); + final SplitPane parent = (SplitPane) dock_parent; + // Remove this dock pane from BorderPane + double dividerPosition = parent.getDividerPositions()[0]; + parent.getItems().remove(this); // Place in split alongside a new dock pane final DockPane new_pane = new DockPane(); dockPaneEmptyListeners.stream().forEach(new_pane::addDockPaneEmptyListener); @@ -651,7 +667,8 @@ else if (dock_parent instanceof SplitDock) setDockParent(split); new_pane.setDockParent(split); // Place that new split in the border pane - parent.addItem(first, split); + parent.getItems().add(split); + parent.setDividerPosition(0, dividerPosition); } else throw new IllegalStateException("Cannot split, dock_parent is " + dock_parent); diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java index 5aa70fb99e..4207028c26 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java @@ -16,11 +16,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; import java.util.UUID; import java.util.logging.Level; -import java.util.stream.Collectors; import javafx.scene.input.MouseEvent; import org.phoebus.framework.jobs.JobManager; @@ -323,6 +320,13 @@ public static BorderPane getLayout(final Stage stage) final Parent layout = stage.getScene().getRoot(); if (layout instanceof BorderPane) return (BorderPane) layout; + if (layout instanceof SplitPane) { + SplitPane splitPane = (SplitPane) layout; + var maybeBorderPane = splitPane.getItems().stream().filter(item -> item instanceof BorderPane).findFirst(); + if (maybeBorderPane.isPresent()) { + return (BorderPane) maybeBorderPane.get(); + } + } throw new IllegalStateException("Expect BorderPane, got " + layout); } @@ -332,10 +336,22 @@ public static BorderPane getLayout(final Stage stage) */ public static Node getPaneOrSplit(final Stage stage) { - final Node container = getLayout(stage).getCenter(); - if (container instanceof DockPane || - container instanceof SplitDock) + Node container = getLayout(stage).getCenter(); + if (container instanceof DockPane || container instanceof SplitDock) { + // Note: the check for "instanceof SplitDock" must occur + // before "instanceof SplitPane" (the next clause), + // since the class SplitDock extends the class + // SplitPane. (Otherwise, the wrong code is run + // for instances of SplitDock.) return container; + } + else if (container instanceof SplitPane) { + SplitPane splitPane = (SplitPane) container; + var maybeDockPaneOrSplitDock = splitPane.getItems().stream().filter(item -> item instanceof DockPane || item instanceof SplitDock).findFirst(); + if (maybeDockPaneOrSplitDock.isPresent()) { + return maybeDockPaneOrSplitDock.get(); + } + } throw new IllegalStateException("Expect DockPane or SplitDock, got " + container); } diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java b/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java index 8c7b28f67c..357176839f 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java @@ -38,6 +38,10 @@ public class SplitDock extends SplitPane */ private Parent dock_parent; + public void setDockParent(Parent dockParent) { + this.dock_parent = dockParent; + } + /** Create a split section * @param dock_parent {@link BorderPane} of {@link SplitDock} * @param horizontally Horizontal? @@ -157,10 +161,21 @@ public void merge() } else if (dock_parent instanceof SplitDock) { + // Note: the check for "instanceof SplitDock" must occur + // before "instanceof SplitPane" (the next clause), + // since the class SplitDock extends the class + // SplitPane. (Otherwise, the wrong code is run + // for instances of SplitDock.) final SplitDock parent = (SplitDock) dock_parent; final boolean was_first = parent.removeItem(this); parent.addItem(was_first, child); } + else if (dock_parent instanceof SplitPane) { + final SplitPane parent = (SplitPane) dock_parent; + // parent.getItems().get(1) == this, parent.getItems().get(0) == Navigator application + // No need to remove 'this' from parent, just update parent.getItems().get(1) to child + parent.getItems().set(1, child); + } else { logger.log(Level.WARNING, "Cannot merge " + this + ", parent is " + dock_parent); diff --git a/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java b/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java index ac2b00a9d7..8cc9287c88 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java @@ -33,7 +33,7 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -class DateTimePane extends GridPane +public class DateTimePane extends GridPane { // Use TimestampFormats private static final StringConverter DATE_CONVERTER = new StringConverter<>() diff --git a/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java b/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java index 276a759f6f..0220d2a6e8 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java @@ -85,7 +85,7 @@ public TimeRelativeIntervalPane(final TemporalAmountPane.Type type) add(abs_end, 2, 1); rel_end.setPadding(new Insets(5)); - add(rel_end, 2, 2); + //add(rel_end, 2, 2); add(new Separator(Orientation.HORIZONTAL), 0, 3, 3, 1); diff --git a/core/ui/src/main/java/org/phoebus/ui/web/HyperLinkRedirectListener.java b/core/ui/src/main/java/org/phoebus/ui/web/HyperLinkRedirectListener.java index 386a5161c8..588cc85600 100644 --- a/core/ui/src/main/java/org/phoebus/ui/web/HyperLinkRedirectListener.java +++ b/core/ui/src/main/java/org/phoebus/ui/web/HyperLinkRedirectListener.java @@ -33,6 +33,8 @@ import org.w3c.dom.html.HTMLAnchorElement; import java.net.URI; +import java.util.Optional; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,19 +50,23 @@ * * this Stackoverflow post. */ -public class HyperLinkRedirectListener implements ChangeListener, EventListener { +public class HyperLinkRedirectListener implements ChangeListener { private static final String CLICK_EVENT = "click"; private static final String ANCHOR_TAG = "a"; private final WebView webView; + private final Optional webClientRoot; + private final Optional> openLogentryWithID; private static final Logger LOGGER = Logger.getLogger(HyperLinkRedirectListener.class.getName()); /** * @param webView The {@link WebView} showing the document. */ - public HyperLinkRedirectListener(WebView webView) { + public HyperLinkRedirectListener(WebView webView, Optional webClientRoot, Optional> openLogentryWithID) { this.webView = webView; + this.webClientRoot = webClientRoot; + this.openLogentryWithID = openLogentryWithID; } @Override @@ -71,20 +77,48 @@ public void changed(ObservableValue observable, State oldValue, for (int i = 0; i < anchors.getLength(); i++) { Node node = anchors.item(i); EventTarget eventTarget = (EventTarget) node; - eventTarget.addEventListener(CLICK_EVENT, this, false); + eventTarget.addEventListener(CLICK_EVENT, + new HyperLinkRedirectEventListener(), // Note: A new instance MUST be created here, otherwise NullPointerExceptions may be thrown when trying to run the event handler! + false); } } } - @Override - public void handleEvent(Event event) { - HTMLAnchorElement anchorElement = (HTMLAnchorElement) event.getCurrentTarget(); - String href = anchorElement.getHref(); - try { - ApplicationService.createInstance("web", new URI(href)); + private class HyperLinkRedirectEventListener implements EventListener { + + private Optional parseLogEntryID(String href) { + if (webClientRoot.isPresent() && openLogentryWithID.isPresent() && href.startsWith(webClientRoot.get())) { + try { + String withoutWebClientRoot = href.substring(webClientRoot.get().length()); + String idString = withoutWebClientRoot.charAt(0) == '/' ? withoutWebClientRoot.substring(1) : withoutWebClientRoot; + long id = Long.parseLong(idString); + return Optional.of(id); + } + catch (Exception exception) { + return Optional.empty(); + } + } + else { + return Optional.empty(); + } + } + + @Override + public void handleEvent(Event event) { event.preventDefault(); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Failed to launch WebBrowserApplication", e); + HTMLAnchorElement anchorElement = (HTMLAnchorElement) event.getCurrentTarget(); + String href = anchorElement.getHref(); + Optional maybeID = parseLogEntryID(href); + if (maybeID.isPresent()) { + Long id = maybeID.get(); + openLogentryWithID.get().accept(id); + } else { + try { + ApplicationService.createInstance("web", new URI(href)); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to launch WebBrowserApplication", e); + } + } } } } diff --git a/core/ui/src/main/resources/icons/checkbox.png b/core/ui/src/main/resources/icons/checkbox.png new file mode 100644 index 0000000000..1a4906fef5 Binary files /dev/null and b/core/ui/src/main/resources/icons/checkbox.png differ diff --git a/core/ui/src/main/resources/icons/checkbox_unchecked.png b/core/ui/src/main/resources/icons/checkbox_unchecked.png new file mode 100644 index 0000000000..56b7608773 Binary files /dev/null and b/core/ui/src/main/resources/icons/checkbox_unchecked.png differ diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java index c5d33d8c7f..5b15bacf46 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.phoebus.applications.saveandrestore.model.ConfigPv; +import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.RestoreResult; import org.phoebus.applications.saveandrestore.model.SnapshotData; import org.phoebus.applications.saveandrestore.model.SnapshotItem; @@ -61,6 +62,7 @@ public void testRestoreFromSnapshotNode() throws Exception { item.setConfigPv(configPv); snapshotData.setSnapshotItems(List.of(item)); + when(nodeDAO.getNode("uniqueId")).thenReturn(Node.builder().name("name").uniqueId("uniqueId").build()); when(nodeDAO.getSnapshotData("uniqueId")).thenReturn(snapshotData); MockHttpServletRequestBuilder request = post("/restore/node?nodeId=uniqueId") diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java index 8a868c7464..743e9c295b 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.phoebus.applications.saveandrestore.model.ConfigurationData; +import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.service.saveandrestore.NodeNotFoundException; import org.phoebus.service.saveandrestore.epics.SnapshotUtil; @@ -50,6 +51,7 @@ public class TakeSnapshotControllerTest { public void testTakeSnapshot() throws Exception { ConfigurationData configurationData = new ConfigurationData(); + when(nodeDAO.getNode("uniqueId")).thenReturn(Node.builder().name("name").uniqueId("uniqueId").build()); when(nodeDAO.getConfigurationData("uniqueId")).thenReturn(configurationData); when(snapshotUtil.takeSnapshot(configurationData)) .thenReturn(Collections.emptyList()); @@ -69,6 +71,7 @@ public void testTakeSnapshot() throws Exception { @Test public void testTakeSnapshotBadConfigId() throws Exception { + when(nodeDAO.getNode("uniqueId")).thenReturn(Node.builder().name("name").uniqueId("uniqueId").build()); when(nodeDAO.getConfigurationData("uniqueId")).thenThrow(new NodeNotFoundException("")); MockHttpServletRequestBuilder request = get("/take-snapshot/uniqueId");