Context-Menu Plugins

Context-menu plugins can provide additional tools and capabilities in specific scenarios within WISER. For example, operations can be provided on datasets or or other objects picked by a user (a point in a dataset, a spectrum, a region of interest, etc.). To do this, the plugin must subclass the wiser.plugins.ContextMenuPlugin type, filling in the various operations that WISER will call.

Implementing a context-menu plugin requires some familiarity with Qt 5, since the plugin must, at a minimum, add QMenu actions for specific operations that are exposed. If a plugin intends to expose its own GUI for configuration or other user interactions, please see GUI Plugins in WISER for more details on how this may be done. You will also need to understand everything in this section so that you can interface effectively with WISER’s internals.

The ContextMenuPlugin Class

Context-menu plugins must derive from the ContextMenuPlugin class. The documentation for this class spells out the essential details for interfacing with WISER.

class wiser.plugins.ContextMenuPlugin[source]

This is the base type for plugins that integrate into WISER pop-up context menus.

add_context_menu_items(context_type: ContextMenuType, context_menu: QMenu, context: Dict[str, Any]) None[source]

This method is called by WISER when it is constructing a context menu, so that the plugin can add any menu-actions relevant to the context.

The type of context is indicated by the context_type argument/enum. Plugins should examine this value and only add menu entries relevant to the context type, to avoid cluttering up the WISER context menus. This is particularly important, as this method may be called multiple times (with different context_type values), in the population of a single context-menu before it is displayed. For example, it is possible for a plugin to see calls to this method with RASTER_VIEW, then DATASET_PICK, then ROI_PICK, in the construction of a single context menu.

Based on the type of context, the context dictionary will contain specific key/value pairs relevant to the context, that the plugin may need for its operation. The details are specified below. Besides these values, the context dictionary will also always contain a wiser key that references a wiser.gui.app_state.ApplicationState object for accessing and manipulating WISER’s internal state in specific ways.

RASTER_VIEW

Indicates a general operation on a dataset within a raster display window - that is, an operation not related to the cursor location. The context dictionary includes these keys:

  • dataset - a reference to the wiser.raster.RasterDataSet object currently being displayed.

  • display_bands - a tuple of integers specifying the bands currently being displayed in the raster-view. This will either hold 1 element if the display is grayscale, or 3 elements if the display is red/green/blue.

SPECTRUM_PLOT

Indicates a general operation within a spectrum-plot window - that is, not related to a specific spectrum or the cursor location. The context dictionary will not have any additional keys.

DATASET_PICK

Indicates a location-specific operation on a dataset within a raster display window - that is, an operation that requires the cursor location. The context dictionary includes these additional keys:

  • dataset - a reference to the wiser.raster.RasterDataSet object currently being displayed.

  • display_bands - a tuple of integers specifying the bands currently being displayed in the raster-view. This will either hold 1 element if the display is grayscale, or 3 elements if the display is red/green/blue.

  • ds_coord - an (int, int) tuple of the pixel in the dataset that was picked by the user.

SPECTRUM_PICK

Indicates a spectrum-specific operation within a spectrum-plot window. The context dictionary will have this additional key:

ROI_PICK

Indicates a region-of-interest-specific operation within a raster display window. The context dictionary includes these additional keys:

  • dataset - a reference to the wiser.raster.RasterDataSet object currently being displayed.

  • display_bands - a tuple of integers specifying the bands currently being displayed in the raster-view. This will either hold 1 element if the display is grayscale, or 3 elements if the display is red/green/blue.

  • roi - a reference to the wiser.raster.RegionOfInterest object that was picked by the user.

  • ds_coord - an (int, int) tuple of the pixel in the dataset that was picked by the user.

Plugins should be careful not to hold onto any context references for too long, as it will generate resource leaks within WISER. A recommended pattern for adding menu actions is as follows:

# Construct a lambda that is called when the QAction is clicked;
# it traps the context dictionary and passes it to the relevant
# handler.  The context is reclaimed when the QAction goes away.
act = context_menu.addAction(context_menu.tr('Some task...'))
act.triggered.connect(lambda checked=False: self.on_some_task(context=context))

The ContextMenuType enumeration is as follows:

enum wiser.plugins.ContextMenuType(value)[source]

This enumeration specifies the kind of context-menu event that occurred, so that plugins know what items to add to the menu.

Valid values are as follows:

RASTER_VIEW = <ContextMenuType.RASTER_VIEW: 1>
SPECTRUM_PLOT = <ContextMenuType.SPECTRUM_PLOT: 2>
DATASET_PICK = <ContextMenuType.DATASET_PICK: 10>
SPECTRUM_PICK = <ContextMenuType.SPECTRUM_PICK: 11>
ROI_PICK = <ContextMenuType.ROI_PICK: 12>

Example Context-Menu Plugin

Here is an example of a simple context menu plugin.

import pprint
import textwrap

from typing import Any, Callable, Dict, List, Optional, Tuple

from wiser.plugins import ContextMenuPlugin, ContextMenuType

from PySide2.QtWidgets import QMenu, QMessageBox


class HelloContextPlugin(ContextMenuPlugin):
    """
    A simple "Hello world!" example of a context-menu plugin.
    """

    def __init__(self):
        super().__init__()

    def add_context_menu_items(
        self,
        context_type: ContextMenuType,
        context_menu: QMenu,
        context: Dict[str, Any],
    ) -> None:
        """
        Use QMenu.addAction() to add individual actions, or QMenu.addMenu() to
        add sub-menus to the Tools menu.
        """
        if context_type == ContextMenuType.RASTER_VIEW:
            """
            Context-menu display in a raster-view, which probably is showing a
            dataset.  The current dataset is passed to the plugin.

            Example code to get all necessary pieces of data for this context_type:
            ```python
            # A RasterDataSet object
            dataset = context["dataset"]
            # A 3 or 1 tuple of integers
            display_bands = context["display_bands"]
            # Every context_type has the app_state in the "wiser" key
            app_state = context["wiser"]
            ```
            """
            pass

        elif context_type == ContextMenuType.SPECTRUM_PLOT:
            """
            Context-menu display in the spectrum-plot window.
            
            While this context_type makes the context dict have
            no additional keys, we stil have the app_state key:

            Example code to get all necessary pieces of data for this context_type:
            ```python
            # Every context_type has the app_state in the "wiser" key
            app_state = context["wiser"]
            ```
            """
            pass

        elif context_type == ContextMenuType.DATASET_PICK:
            """
            A specific dataset was picked.  This may not be in the context of
            a raster-view window, e.g. if the user right-clicks on a dataset
            in the info viewer.
            
            Example code to get all necessary pieces of data for this context_type:
            ```python
            # A RasterDataSet object
            dataset = context["dataset"]
            # A 3 or 1 tuple of integers
            display_bands = context["display_bands"]
            # An (int, int) tuple of the clicked pixel in the dataset above
            ds_coord = context["ds_coord"]
            # Every context_type has the app_state in the "wiser" key
            app_state = context["wiser"]
            ```
            """
            pass

        elif context_type == ContextMenuType.SPECTRUM_PICK:
            """
            A specific spectrum was picked.  The spectrum is passed to the
            plugin.
            
            Example code to get all necessary pieces of data for this context_type:
            ```python
            # A Spectrum object
            spectrum = context["spectrum"]
            # Every context_type has the app_state in the "wiser" key
            app_state = context["wiser"]
            ```
            """
            pass

        elif context_type == ContextMenuType.ROI_PICK:
            """
            A specific ROI was picked.  The ROI is passed, along with the
            current dataset (if available).
            
            Example code to get all necessary pieces of data for this context_type:
            ```python
            # A RasterDataSet object
            dataset = context["dataset"]
            # A 3 or 1 tuple of integers
            display_bands = context["display_bands"]
            # A RegionOfInterest object that was picked by the user
            roi = context["roi"]
            # An (int, int) tuple of the clicked pixel in the dataset above
            ds_coord = context["ds_coord"]
            # Every context_type has the app_state in the "wiser" key
            app_state = context["wiser"]
            ```
            """
            pass

        else:
            raise ValueError(f"Unrecognized context_type value {context_type}")

        act = context_menu.addAction(f"Say hello {context_type}...")
        act.triggered.connect(lambda checked=False: self.say_hello(context_type, context))

    def say_hello(self, context_type: ContextMenuType, context: Dict[str, Any]):
        context_str = pprint.pformat(context)

        print("HelloContextPlugin.say_hello() was called!")
        print(f" * context_type = {context_type}")
        print(f' * context =\n{textwrap.indent(context_str, " " * 8)}')

        QMessageBox.information(
            None,
            "Hello-Context Plugin",
            f"Hello from a {context_type} context menu!\n\n{context_str}",
        )