diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 134e2a2101..95e3086e78 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -61,6 +61,31 @@ test.describe('Example Imagery Object', () => { await expect(page.locator('.c-hud')).toBeHidden(); }); + test('Can right click on image and open it in a new tab', async ({ page, context }) => { + // try to right click on image + const backgroundImage = await page.locator(backgroundImageSelector); + await backgroundImage.click({ + button: 'right', + // eslint-disable-next-line playwright/no-force-option + force: true + }); + // expect context menu to appear + await expect(page.getByText('Save Image As')).toBeVisible(); + await expect(page.getByText('Open Image in New Tab')).toBeVisible(); + + // click on open image in new tab + const pagePromise = context.waitForEvent('page'); + await page.getByText('Open Image in New Tab').click(); + // expect new tab to be in browser + const newPage = await pagePromise; + await newPage.waitForLoadState(); + // expect new tab url to have jpg in it + await expect(newPage.url()).toContain('.jpg'); + }); + + // this requires CORS to be enabled in some fashion + test.fixme('Can right click on image and save it as a file', async ({ page }) => {}); + test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName diff --git a/src/plugins/imagery/actions/OpenImageInNewTabAction.js b/src/plugins/imagery/actions/OpenImageInNewTabAction.js new file mode 100644 index 0000000000..aa7d1525aa --- /dev/null +++ b/src/plugins/imagery/actions/OpenImageInNewTabAction.js @@ -0,0 +1,46 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +export default class OpenImageInNewTabAction { + constructor(openmct) { + this.openmct = openmct; + + this.cssClass = 'icon-new-window'; + this.description = 'Open the image in a new tab'; + this.group = 'action'; + this.key = 'openImageInNewTab'; + this.name = 'Open Image in New Tab'; + this.priority = 1; + } + + invoke(objectPath, view) { + const viewContext = (view.getViewContext && view.getViewContext()) || {}; + window.open(viewContext.imageUrl, '_blank').focus(); + } + + appliesTo(objectPath, view = {}) { + const viewContext = (view.getViewContext && view.getViewContext()) || {}; + if (!viewContext.imageUrl) { + return false; + } + } +} diff --git a/src/plugins/imagery/actions/SaveImageAsAction.js b/src/plugins/imagery/actions/SaveImageAsAction.js new file mode 100644 index 0000000000..fd228ea7df --- /dev/null +++ b/src/plugins/imagery/actions/SaveImageAsAction.js @@ -0,0 +1,67 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +export default class SaveImageAction { + constructor(openmct) { + this.openmct = openmct; + + this.cssClass = 'icon-save-as'; + this.description = 'Save image to file'; + this.group = 'action'; + this.key = 'saveImageAs'; + this.name = 'Save Image As'; + this.priority = 1; + } + + async invoke(objectPath, view) { + const viewContext = (view.getViewContext && view.getViewContext()) || {}; + try { + const filename = + viewContext.imageUrl.split('/').pop().split('#')[0].split('?')[0] || 'downloaded-image.png'; + const response = await fetch(viewContext.imageUrl); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + + // Create a temporary anchor element and trigger the download + const a = document.createElement('a'); + a.href = blobUrl; + a.download = filename; // Set the filename for the download + + // Append anchor to body, trigger click, then remove it from the DOM + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Revoke the blob URL after the download + URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('Could not download the image.', error); + } + } + + appliesTo(objectPath, view = {}) { + const viewContext = (view.getViewContext && view.getViewContext()) || {}; + if (!viewContext.imageUrl) { + return false; + } + } +} diff --git a/src/plugins/imagery/components/AnnotationsCanvas.vue b/src/plugins/imagery/components/AnnotationsCanvas.vue index f0ee1f8660..b22c13610b 100644 --- a/src/plugins/imagery/components/AnnotationsCanvas.vue +++ b/src/plugins/imagery/components/AnnotationsCanvas.vue @@ -28,6 +28,7 @@ @mousedown="clearSelectedAnnotations" @mousemove="trackAnnotationDrag" @click="selectOrCreateAnnotation" + @contextmenu="showContextMenu" > @@ -43,8 +44,10 @@ const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)'; const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC'; const SELECTED_ANNOTATION_FILL_STYLE = 'rgba(199, 87, 231, 0.2)'; +const CONTEXT_MENU_ACTIONS = ['openImageInNewTab', 'saveImageAs']; + export default { - inject: ['openmct', 'domainObject', 'objectPath'], + inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], props: { image: { type: Object, @@ -481,6 +484,21 @@ export default { drawnRectangles.push(annotationRectangle); } }); + }, + showContextMenu: function (event) { + event.preventDefault(); + + let objectPath = this.objectPath; + + const actions = CONTEXT_MENU_ACTIONS.map((key) => this.openmct.actions.getAction(key)); + const menuItems = this.openmct.menus.actionsToMenuItems( + actions, + objectPath, + this.currentView + ); + if (menuItems.length) { + this.openmct.menus.showMenu(event.x, event.y, menuItems); + } } } }; diff --git a/src/plugins/imagery/plugin.js b/src/plugins/imagery/plugin.js index 9d81df0e84..2b59ae3152 100644 --- a/src/plugins/imagery/plugin.js +++ b/src/plugins/imagery/plugin.js @@ -20,6 +20,8 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import OpenImageInNewTabAction from './actions/OpenImageInNewTabAction.js'; +import SaveImageAsAction from './actions/SaveImageAsAction.js'; import ImageryTimestripViewProvider from './ImageryTimestripViewProvider.js'; import ImageryViewProvider from './ImageryViewProvider.js'; @@ -27,5 +29,7 @@ export default function (options) { return function install(openmct) { openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options)); openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct)); + openmct.actions.register(new OpenImageInNewTabAction(openmct)); + openmct.actions.register(new SaveImageAsAction(openmct)); }; }