Extending DAVE#

When adding (proprietary) elements to DAVE you may want to extend the GUI without changing the source-code of DAVE itself.

This section describes how to integrate

  • workspaces

  • dockwidgets

  • nodes

into the GUI.

Note

The DAVE gui is open-source. If the functionality is insufficient for your purpose then you may implement this yourself and be added to the DAVE Gui via a PR on github.

New Node types#

Registering#

Registering the class#

DAVE executes python code when importing files, copying the scene or adding nodes.

New classes need to be made available to the environment when running code in order to be usable.

This is done by adding them to a global setting DAVE_ADDITIONAL_RUNTIME_MODULES:

# Add the balloon class to the modules that are available when executing code

from DAVE.settings import DAVE_ADDITIONAL_RUNTIME_MODULES
DAVE_ADDITIONAL_RUNTIME_MODULES['Balloon'] = Balloon

Registering the properties and documentation#

The properties of the and their documentation need to be added to DAVE as well. This makes them available for timelines, limits derived-properties, reporting and others.

The properties are registered in the following nested dictionary:

from DAVE.settings import DAVE_NODEPROP_INFO,NodePropertyInfo

# type is the node-type, for example Point
# property-name is the name of the property, for example 'global_position'
info = DAVE_NODEPROP_INFO[type][property_name]

# info is a NodePropertyInfo object

The properties of nodes are used in various places in the guis and reporting:

  • The limits screen to define limits for single numerical properties

    • Properties with a single numerical value

  • The derived properties screen - to show the values of derived properties

    • Anything interesting, typically numerical and boolean values and sequences of those

      • type = float, bool

  • Reporting module

    • All properties that have a physical meaning and/or define how the model is structured (parent, meshes, etc)

  • Timeline module

    • All settable single properties, including nodes and booleans

      • type = float/bool/node

      • settable = True

  • Exploration module

    • All settable single numerical properties

Besides that we need to have the following documentation available for each node/property combination:

  • short description (1 line)

  • long description

  • unit, eg [m]

  • remarks, eg (global) or (parent)

    Here the property may be inherited and/or overridden.

  • settable: not read-only

  • single-settable: can be used in time-line - bool, Node,

  • single-numeric: can be used as limit

Class

Property

Short doc

Long doc

Unit

Remarks

type

settable

single-settable

single-numeric

Point

x

x-position

x-position…

[m]

(parent)

float

X

X

X

Point

parent

parent

parent…

(Node)

Frame

X

X

Node

name

name

name…

(unique)

str

X

Point

position

position

position…

[m,m,m]

(parent)

float

X

Point

applied_force

force

force …

[kN,kN,kN]

(parent)

float

X

Point

force

|force|

force …

[kN]

float

X

Frame

fixed_x

bool

X

X

Frame

fixed

bool

X

Node

visible

visible in GUI

bool

X

X

Cable

connections

Point|Circle

X

How, where and when#

When looking up the properties this will be done using a Node object and optionally a property name (these two are therefore the index of the database). Matching class needs to be done on an is-instance basis and considering overriding.

Say:

class A has property p
class B derives from A and overrides property p
class C derives from B

How

requesting the documentation for property p on a node of class C should give the documentation of p of class B. This can be done using the mro. The property of the class with the lowest index in the mro of the node class is the one we want.

The data is stored as a nested dictionary:

data[class][propname(str)] = node_property_info

with NodePropertyInfo a dataclass

There is a convenience function to register:

from DAVE.settings import register_nodeprop

register_nodeprop(HEBO_CraneVessel,'hoist_length_main_sb',
                 property_type=float,
                 doc_short="SB main hoist length",
                 doc_long="SB main hoist length",
                 units = "m",
                 remarks="",
                 is_settable=True,
                 is_single_numeric=True,
                 is_single_settable=True)

Where and when

To store the data with the class as key, the class needs to be defined. So this info can only be created after definition of the classes.

It makes sense to read the data from a .csv or pickle file, but those can not store classes. This means resolving the class from the class name which is easy enough via either globals() or, even better, the DAVE_ADDITIONAL_RUNTIME_MODULES dict.

DAVE itself has an example of how to do this (in nodes.py) . This example is copied here fore convenience:

cdir = Path(dirname(__file__))
filename = cdir / './resources/node_prop_info.csv'
from DAVE.settings import DAVE_NODEPROP_INFO, NodePropertyInfo

if filename.exists():

    types = DAVE_ADDITIONAL_RUNTIME_MODULES.copy()
    types['tuple'] = tuple
    types['int'] = int
    types['float'] = float
    types['bool'] = bool
    types['str'] = str

    btypes = dict()
    btypes['True'] = True
    btypes['False'] = False
    btypes['true'] = True
    btypes['false'] = False


    with open(filename, newline='') as csvfile:
        prop_reader = csv.reader(csvfile)
        header = prop_reader.__next__()  # skip the header
        for row in prop_reader:
            cls_name = row[0]
            cls = DAVE_ADDITIONAL_RUNTIME_MODULES[cls_name]

            prop_name = row[1]
            val_type = types[row[2]]

            info = NodePropertyInfo(node_class=cls,
                                    property_name=row[1],
                                    property_type=val_type,
                                    doc_short=row[3],
                                    units=row[4],
                                    remarks=row[5],
                                    is_settable=btypes[row[6]],
                                    is_single_settable=btypes[row[7]],
                                    is_single_numeric=btypes[row[8]],
                                    doc_long=row[9])

            if cls not in DAVE_NODEPROP_INFO:
                DAVE_NODEPROP_INFO[cls] = dict()
            DAVE_NODEPROP_INFO[cls][prop_name]=info

else:
    print(f'Could not register node property info because {filename} does not exist')

Creating a new dock#

Docks are floating or docked sub-windows of the main GUI. Everything except the 3d view-port and the menu is a dock.

Docks are QtWidgets derived from guiDockWidget

Docks are created and activated by main. Upon creation they receive some references to things in the gui:

d.guiScene = self.scene
d.guiEmitEvent = self.guiEmitEvent
d.guiRunCodeCallback = self.run_code
d.guiSelectNode = self.guiSelectNode
d.guiSelection = self.selected_nodes
d.guiPressSolveButton = self.solve_statics
d.gui = self

Communication between the docks is done via the guiEmitEvent which can send any of the ENUMs defined in guiEventType. Handling of these events is to be implemented in guiProcessEvent.

Registering#

Any dock needs to be added to the DAVE_GUI_DOCKS dictionary. The keys in this dictionary are strings. These strings are also used as the dock-window title.

from DAVE.gui.dockwidget import DAVE_GUI_DOCKS
DAVE_GUI_DOCKS['Balloon'] = BalloonDock

Activating a dock#

See workspaces

Integrating into the main gui#

Code can be executed at various moment by defining plugin-functions and adding a reference to that function to a globally available list.

Init#

plugin_init is executed at the end of the .init function. It takes a single parameter being the instance of the Gui that is being intialized ( the self of the init function)

def my_function(gui):
	print('executed on startup')

from DAVE.gui.main import DAVE_GUI_PLUGINS_INIT
DAVE_GUI_PLUGINS_INIT.append(my_function)

Workspaces#

The user can activate a workspace by pressing one of the buttons at the top side of the interface. Internally a workspace is identified by an WORKSPACE_ID which is a unique, upper-case string.

Adding a button#

Adding a button can be done by adding (or inserting) an entry to the DAVE_GUI_WORKSPACE_BUTTONS list. This is a tuple where the first entry is the text on the button and the second entry is the workspace id

from DAVE.gui.main import DAVE_GUI_WORKSPACE_BUTTONS
DAVE_GUI_WORKSPACE_BUTTONS.append(('BaLLoooon !', 'BALLOON'))

image-20220203142649314

Activating the workspace#

Workspaces are controlled by the “activate_workspace” function. This function typically creates one of more dockwidgets.

A plugin can be registered in DAVE_GUI_PLUGINS_WORKSPACE

def my_plugin_activate_workspace(gui, workspacename):
    print('calling activateworkspace')
    if workspacename.upper() == 'BALLOON': 
        gui.close_all_open_docks() # close all other docks (optional)
        gui.show_guiWidget('Balloon')  # <-- ID of the balloon dock
        
    if worspacename.upper() == 'CONSTRUCT':   # <--- you can also act on activation of other docks
        gui.show_guiWidget('Balloon')  # <-- ID of the balloon dock
        
from DAVE.gui.main import DAVE_GUI_PLUGINS_WORKSPACE
DAVE_GUI_PLUGINS_WORKSPACE.append(my_plugin_activate_workspace)

Context-menu#

The context-menu is the right-click menu. This is used to add nodes. A plugin here has the following signature:

openContextMenyAt(menu, node_name, gui)

the plugin gets called with the following arguments:

  1. QMenu object

  2. The name of the first selected node (if any)

  3. A reference to the main gui

the reference to the main gui can be used to run code as follows:

def plugin_context(menu, node_name,gui):
    code = 'Balloon(s, s.available_name_like("new_balloon"))'
    def action():
        gui.run_code(code, guiEventType.MODEL_STRUCTURE_CHANGED)

    BalloonIcon = QIcon(str(Path(__file__).parent / 'balloon.png'))

    action = menu.addAction("Add balloon", action)
    action.setIcon(BalloonIcon)

The plugin needs to be registered in:

from DAVE.gui.main import DAVE_GUI_PLUGINS_CONTEXT
DAVE_GUI_PLUGINS_CONTEXT.append(plugin_context)

Editing nodes#

Selecting a node while in “construction” makes the “Properties” dock-widget visible.

Extending the node-editor#

Sections in the node-propeties widget derive from NodeEditor

  1. Create a new MyNewNodeEditor class which derives from NodeEditor

  2. Register the node-editor class against the node-instances for which the editor should open.

from DAVE.gui.widget_nodeprops import DAVE_GUI_NODE_EDITORS
DAVE_GUI_NODE_EDITORS[Balloon] = BalloonNodeEditor

# Note: Balloon is the Node-Class, BalloonNodeEditor is the editor class

images/image-20220203143548291

Alternatively#

Create a new dock that reacts to the “SELECTION_CHANGED” event.