Skip to content

Pyut Plugin Development

Humberto Sanchez II edited this page Jan 18, 2025 · 14 revisions

Pyut Plugin Development

Introduction

Developing a plugin is the easiest way to enhance Pyut functionality. Developers can extend the types of data Pyut can export and the types of data or documents it can produce. Additionally, Pyut plugins can extend the functionality and behavior of the editor.

Definitions

OGL Object Graphics Library. This is a custom Pyut library not related to the wxPython library of the same name.

Plugin Types

Developers implement a particular interface depending on the type behavior or input/output that a developer wishes to implement.

Plugin Adapter

In order to keep the plugins loosely coupled from the Pyut internals, plugins request information and data from Pyut via a Plugin Adapter. The following diagram shows the class diagram depiction.

Notice that various methods require a callback. Again, in order to loosely couple the plugins from Pyut the PluginManager sends request events to Pyut. Pyut responds by supplying the information via a parameter on the supplied callback.

classDiagram
    direction RL
    %% Links follow
    IPluginAdapter  --  ScreenMetrics : 
    class ScreenMetrics { 
        +int screenWidth
        +int screenHeight
        +int dpiX
        +int dpiY
    }
    class IPluginAdapter { 
        +str pyutVersion
        +ScreenMetrics screenMetrics
        +str currentDirectory
        + __init__()
        + getFrameSize(callback)
        + getFrameInformation(callback)
        + getSelectedOglObjects(callback)
        + getObjectBoundaries(callback)
        + refreshFrame()
        + selectAllOglObjects()
        + deselectAllOglObjects()
        + addShape(shape)
        + loadProject(pluginProject)
        + requestCurrentProject(callback)
        + indicatePluginModifiedProject()
        + deleteLink(oglLink)
        + createLink(linkInformation, callback)
    }
Loading

Tool Plugins

Tool plugins are the simplest to write. Essentially, they manipulate the current UML frame.

Developers implement a Pyut Tool by creating a class that implements ToolPluginInterface. See the following UML diagram. The developer fills in the code in the doAction method. If the plugin requires some intervention prior to executing the developer implements that code in the setOptions method. For example, the plugin may ask a question or present a dialog of options to set prior to execution. If the method returns False the PluginManager assumes that the user cancelled the action.

classDiagram
    direction RL
    %% Links follow
    BasePluginInterface<|--ToolPluginInterface
    class ToolPluginInterface { 
        +str menuTitle
        + __init__(pluginAdapter)
        + executeTool()
        +bool setOptions()
        + doAction()
    }
    class BasePluginInterface { 
        +PluginName name
        +str author
        +str version
        +InputFormat inputFormat
        +OutputFormat outputFormat
        + __init__(pluginAdapter)
        + displayNoUmlFrame(cls)
        + displayNoSelectedOglObjects(cls)
        +SingleFileRequestResponse askForFileToImport(startDirectory)
        +MultipleFileRequestResponse askToImportMultipleFiles(startDirectory)
        +SingleFileRequestResponse askForFileToExport(defaultFileName, defaultPath)
        +ImportDirectoryResponse askForImportDirectoryName()
        +ExportDirectoryResponse askForExportDirectoryName(preferredDefaultPath)
        # _layoutUmlClasses(oglClasses)
        # _layoutLinks(oglLinks)
        -str __composeWildCardSpecification()
    }
Loading

Input/Output Plugins

I/O plugins are more complex to author. Their purpose is to allow the import of structured external data that can be transformed into a UML diagram. Conversely, the plugin may export Pyut UML diagrams into a structured external format.

  • A developer exports Pyut UML objects to a different format via the .write() method.
  • A developer imports structured external data and creates Pyut UML objects via the .read() method.
  • If the developer needs additional information from the end user prior to invocation of the .write() method he should implement the setExportOptions method.
  • Conversely, the developer implements the setImportOptions method to get additional information prior to execution of read method.

The developer can choose to implement either of the methods or both if appropriate.

Note

Note that the write and read methods return True or False to indicate success or failure.

The Plugin Manager determines the plugin type based on whether the inputFormat or outputFormat properties return non-null values.

classDiagram
    direction RL
    %% Links follow
    BasePluginInterface<|--IOPluginInterface
    class BasePluginInterface { 
        +PluginName name
        +str author
        +str version
        +InputFormat inputFormat
        +OutputFormat outputFormat
        + __init__(pluginAdapter)
        + displayNoUmlFrame(cls)
        + displayNoSelectedOglObjects(cls)
        +SingleFileRequestResponse askForFileToImport(startDirectory)
        +MultipleFileRequestResponse askToImportMultipleFiles(startDirectory)
        +SingleFileRequestResponse askForFileToExport(defaultFileName, defaultPath)
        +ImportDirectoryResponse askForImportDirectoryName()
        +ExportDirectoryResponse askForExportDirectoryName(preferredDefaultPath)
        # _layoutUmlClasses(oglClasses)
        # _layoutLinks(oglLinks)
        -str __composeWildCardSpecification()
    }
    class IOPluginInterface { 
        + __init__(pluginAdapter)
        + executeImport()
        # _executeImport(frameInformation)
        + executeExport()
        # _executeExport(frameInformation)
        +bool setImportOptions()
        +bool setExportOptions()
        +bool read()
        + write(oglObjects)
    }
Loading

Simple Tool Plugin

This section details a simple tool plugin that does a simple transform on the currently selected OGL objects on the current UML Frame.

The first task is to create the class and subclass it from the abstract class ToolPluginInterface. Additionally, the developer should set the following protected class variables to rationale values.

Class Constructor

  • _name -- The plugin name
  • _author -- The plugin's author
  • _versions -- The plugin version
  • _menuTitle -- The text to display on the Pyut Tools Menu
class ToolTransforms(ToolPluginInterface):
    """
     A plugin for making transformations : translation, rotations, ...
    """
    def __init__(self, pluginAdapter: IPluginAdapter):

        super().__init__(pluginAdapter=pluginAdapter)

        self.logger: Logger = getLogger(__name__)

        self._name      = PluginName('Transformations')
        self._author    = 'C.Dutoit/Humberto A. Sanchez II'
        self._version   = '1.5'

        self._menuTitle = 'Transformation X/Y'

        self._transformX: int = 0
        self._transformY: int = 0

Notice we declared a couple of tool instance variable that we will reference later.

Implement the setOptions method

This tool plugin needs to know how much to transform the UML objects in the x and y direction. I am not going to show the DlgTransforms implementation. I will leave it as an exercise for the reader. Essentially, the plugin needs to know how much to translate the selected objects in either the X or Y direction. Notice that we save the input values to the instance variables we created in the constructor.

    def setOptions(self) -> bool:

        with DlgTransforms(parent=NO_PARENT_WINDOW) as dlg:
            if dlg.ShowModal() == OK:
                self._transformX = dlg.transformX
                self._transformY = dlg.transformY
                proceed: bool = True
            else:
                proceed = False

        return proceed

Implement the doAction method

Important

Notice that by default plugins require object selection.

The ToolPluginInterface plumbing populates the _selectedOglObjects variable. The code iterates through the selected objects and changes their x,y positions. At the end the method uses the PluginAdapter to indicate to Pyut that it modified the project. Additionally, it asks Pyut to refresh the UML frame since it moved various objects.

    def doAction(self):

        selectedObjects: OglObjects = self._selectedOglObjects

        for obj in selectedObjects:
            oglObject: OglObject = cast(OglObject, obj)
            x, y = oglObject.GetPosition()
            newX: int = x + self._transformX
            newY: int = y + self._transformY

            self.logger.info(f'x,y: {x},{y} - {newX=} {newY=}')
            oglObject.SetPosition(newX, newY)

        self._pluginAdapter.indicatePluginModifiedProject()
        self._pluginAdapter.refreshFrame()

Simple Input Only I/O Plugin

Class Constructor

The IODTD plugin is an example of an input only IO Plugin. Its function is to read a DTD file and convert it into a UML class diagram. The constructor for it is as follows.

FORMAT_NAME:        FormatName        = FormatName('DTD')
PLUGIN_EXTENSION:   PluginExtension   = PluginExtension('dtd')
PLUGIN_DESCRIPTION: PluginDescription = PluginDescription('W3C DTD 1.0 file format')


    def __init__(self, pluginAdapter: IPluginAdapter):
        super().__init__(pluginAdapter)

        # from super class
        self._name       = PluginName('IoDTD')
        self._author   = 'C.Dutoit <[email protected]>''
        self._version = '1.0'
        self._inputFormat  = InputFormat(formatName=FORMAT_NAME, extension=PLUGIN_EXTENSION, description=PLUGIN_DESCRIPTION)
        self._outputFormat = cast(OutputFormat, None)

        self._fileToImport: str = ''

Notice that I/O plugin developers provide the _name, _author, and _version values. Additionally, I/O plugins need to declare the input and output format. In this case, IODTD sets its ouput format to None since it does not support it. IODTD declares its format name, a default input format of dtd and in the description declares the specific supported DTD version.

IODTD also declares a protected instance variable that holds the imported file name.

User Input

The following shows the implementation of both the .setImportOptions and .setExportOptions

    def setImportOptions(self) -> bool:
        """
        We do need to ask for the input file name

        Returns:  'True', we support import
        """
        response: SingleFileRequestResponse = self.askForFileToImport(startDirectory=None)
        if response.cancelled is True:
            return False
        else:
            self._fileToImport = response.fileName

        return True

    def setExportOptions(self) -> bool:
        return False

IODTD does not support exporting thus, setExportOptions always returns False. The setImportOptions method pops up a dialog to determine the input file. Notice that the method uses a BasePluginInterface.askForFileToImport() helper method. The method is smart enough to only allow selection files of the appropriate input type. See the graphic of the selection dialog below.

AskForFileToImport

Implement the .read method

The read method uses a helper class that knows how to parse DTD files and can create OGL Classes and OGL Links. I will not delve into its implementation. The method then places each of the UML Classes and the appropriate OGL Links on the UML diagram. At the end it request a frame refresh and indicates that the UML diagram was modified.

    def read(self) -> bool:
        """

        Returns:  True if import succeeded, False if error or cancelled
        """
        filename: str = self._fileToImport

        dtdParser: DTDParser = DTDParser()

        dtdParser.open(filename=filename)

        oglClasses: OglClasses = dtdParser.oglClasses
        for oglClass in oglClasses:
            self._pluginAdapter.addShape(oglClass)

        oglLinks: OglLinks = dtdParser.links
        for oglLink in oglLinks:
            self._pluginAdapter.addShape(oglLink)

        self._pluginAdapter.refreshFrame()
        wxYield()
        self._pluginAdapter.indicatePluginModifiedProject()

        return True

Simple Output Only I/O Plugin

Class Constructor

The IOWxImage plugin is an example of an output only IO Plugin. Its function is to provide an image of the current UML diagram. It can create images in different formats. The constructor for it is as follows.

FORMAT_NAME:        FormatName        = FormatName('Wx Image')
PLUGIN_EXTENSION:   PluginExtension   = PluginExtension('png')
PLUGIN_DESCRIPTION: PluginDescription = PluginDescription('png, bmp, tiff, or jpg')


def __init__(self, pluginAdapter: IPluginAdapter):
        """

        Args:
            pluginAdapter:  A class that implements IPluginAdapter
        """
        super().__init__(pluginAdapter=pluginAdapter)

        self.logger: Logger = getLogger(__name__)

        self._name    = PluginName('Wx Image')
        self._author  = 'Humberto A. Sanchez II'
        self._version = '0.90'

        self._inputFormat  = cast(InputFormat, None)
        self._outputFormat = OutputFormat(formatName=FORMAT_NAME, extension=PLUGIN_EXTENSION, description=PLUGIN_DESCRIPTION)

        self._autoSelectAll = True     # we are taking a picture of the entire diagram

        self._imageFormat:    WxImageFormat = cast(WxImageFormat, None)
        self._outputFileName: str                   = cast(str, None)

Notice that I/O plugin developers provide the _name, _author, and _version values. Additionally, I/O plugins need to declare the input and output format. In this case, IOWxImage sets its input format to none since it does not support it. IOWxImage declares its format name, a default output format of .png and in the description declares the other output formats.

IOWxImage also requests that the IOPluginInterface auto select all the the UML objects on the UML diagram frame.

IOWxImage also declares a pair of protected instance variables that hold the values of information that the plugin will request from the end-user.

User Input

The following shows the implementation of both the .setImportOptions and .setExportOptions.

    def setImportOptions(self) -> bool:
        return False

    def setExportOptions(self) -> bool:
        """
        Popup the options dialog

        Returns:
            if False, the export will be cancelled.
        """
        with DlgWxImageOptions(None) as dlg:
            if dlg.ShowModal() == OK:
                self.logger.warning(f'{dlg.imageFormat=} {dlg.outputFileName=}')
                self._imageFormat        = dlg.imageFormat
                self._outputFileName = dlg.outputFileName

            else:
                self.logger.warning(f'Cancelled')
                return False
        return True

IOWxImage does not support importing thus setImportOptions always returns False. The setExportOptions method pops up a dialog to determine where the output file should be placed and what format it should be. I will not show its implementation, but the following is a graphic of the dialog.

DlgWxImageOptions

Notice that once the end-user presses Ok, the code retrieves the image format and outputFileName

Next the IO Plugin developer implements the write method. Prior to invoking this method, the PluginManager via the IOPluginInterface plumbing passes the UML objects on the current frame to this method. Since in the constructor the plugin specified _autoSelectAll = True, this method get a list of all the objects. This particular plugin does not use them. The createScreenImageFile() method is a plugin common method that knows how to create an image file. It returns an error the write method displays an error log.

Implement .write Method

from pyutplugins.common.Common import createScreenImageFile

from pyutplugins.ioplugins.wximage.WxImageFormat import WxImageFormat

def write(self, oglObjects: OglObjects):
        """
        Write data

        Args:
            oglObjects:     list of exported objects
        """
        pluginAdapter:       IPluginAdapter      = self._pluginAdapter
        frameInformation: FrameInformation = self._frameInformation
        pluginAdapter.deselectAllOglObjects()

        imageType: BitmapType = WxImageFormat.toWxBitMapType(self._imageFormat)
        extension: str                = self._imageFormat.__str__()
        filename:  str                 = f'{self._outputFileName}.{extension}'

        status: bool = createScreenImageFile(frameInformation=frameInformation,
                                             imagePath=Path(filename),
                                             imageType=imageType)
        if status is False:
            msg: str = f'Error on image write to {filename}'
            self.logger.error(msg)
            MessageBox(message=msg, caption='Error', style=OK)

Final Thoughts

The plugin module provides an easy way to extend Pyut functionality. The developer can work on the currently displayed elements, or can export them to another format, and finally the developer can import data that fits the UML model