Step 4: Add new editors

Edit new Scenario properties

Recall that we have added new fields to the Scenario EClass, so let’s add those fields to the Scenario editor.

Go to the '…​application' bundle and find the '…​pages' package. Open the 'ScenarioPage.java' file. In its 'createControlsInternal()' method, add the following block of code:

ScenarioPage.java, enabling editing new fields
addNumericSection("Max delivery time, hrs", DatamodelPackage.Literals.SCENARIO__MAX_DELIVERY_TIME_HRS)
    .addTextbox(UpdateValueStrategyFactory.doublePositive());
addDistributionSection("Interval between requests, hrs", DatamodelPackage.Literals.SCENARIO__INTERVAL_BETWEEN_REQUESTS_HRS)
    .addTextbox(UpdateValueStrategyFactory.distribution())
    .addDialogButton("...", DatamodelPackage.Literals.SCENARIO__INTERVAL_BETWEEN_REQUESTS_HRS)
    .setEnabled(false);

Start the desktop application. In the application, create a new scenario (File → New). Click the 'Scenario' tree item in the 'Scenario structure':

Editors for new Scenario fields

Note the two new editable fields:

  1. The first field editor allows only positive double values to be entered for the max delivery time.

  2. If you click the '…​' button next to the last field, a distribution setup window appears. You can select the distribution type and tune the distribution parameters (such as the mean value).

Add new scenario tree items

We will now add new tree items: three for assets (parent 'Assets' item plus two child items, for Warehouses and Stores) and one for trucks.

First, open the TreeElementType enum (located in the …​application.utils package). It already contains four element types: NETWORK, ARC, NODE, and SCENARIO.

Add another four element types:

TreeElementType.java, new tree element types
ASSET("/icons/object.png", "Assets"),
WAREHOUSE("/icons/object.png", "Warehouses"),
STORE("/icons/object.png", "Stores"),
TRUCK("/icons/object.png", "Trucks"),

All four new scenario tree items will have the same icon, 'object.png'.

Create a new TreeElementAssets class in the …​application.parts.editor.treeelements package:

TreeElementAssets.java
package com.company.tutorial3.application.parts.editor.treeelements;

import java.util.List;

import com.company.tutorial3.application.utils.TreeElementType;
import com.company.tutorial3.datamodel.Scenario;

public class TreeElementAssets extends TreeElement {
    private Scenario scenario;

    public TreeElementAssets(Scenario scenario) {
        super(TreeElementType.ASSET, scenario);
        this.scenario = scenario;
    }

    @Override
    protected List<TreeElement> createChildElements() {
        return List.of(
                createLeaf(TreeElementType.WAREHOUSE, () -> scenario.getWarehouses().size()),
                createLeaf(TreeElementType.STORE, () -> scenario.getStores().size())
                );
    }
}

Update the createChildElements() method in the TreeElementScenario class:

TreeElementScenario.java, createChildElements() method
@Override
protected List<TreeElement> createChildElements() {
    return List.of( new TreeElementNetwork(scenario), 
                    new TreeElementAssets(scenario),
                    createLeaf(TreeElementType.TRUCK, () -> scenario.getTrucks().size()) );
}

Start the application, create a new scenario and note the new items in the updated scenario tree:

New items in scenario tree

Add object tables for new scenario tree items

When the user selects an item in the scenario tree, an list of objects is shown in the bottom left part. This is already implemented for nodes and arcs. Let’s add similar tables for assets and trucks.

Find the ObjectsPart class in the …​application.parts.editor package. In its registerPages() method, add the following lines:

ObjectsPart.java, new tables in registerPages() method
new ObjectsPage<Warehouse>(this, DatamodelPackage.Literals.SCENARIO__WAREHOUSES, TreeElementType.WAREHOUSE, 
        ()-> {
            var wh = DatamodelFactory.eINSTANCE.createWarehouse();
            wh.setScenario(scenarioObservable.getValue());
            return wh;
        }, null, null)
        .setTableRefreshBinding(DatamodelPackage.Literals.ASSET__ID, DatamodelPackage.Literals.ASSET__NAME, DatamodelPackage.Literals.ASSET__NODE)
        .setAfterCreateTableElementAction(tableView -> {
            tableView.addColumn("ID", 100, Warehouse::getId);
            tableView.addColumn("Name", 100, p -> p == null ? "" : p.getName());
            tableView.addColumn("Node", 100, Warehouse::getNode, p -> p == null ? "" : p.getName());
        });

new ObjectsPage<Store>(this, DatamodelPackage.Literals.SCENARIO__STORES, TreeElementType.STORE, 
        () -> {
            var store = DatamodelFactory.eINSTANCE.createStore();
            store.setScenario(scenarioObservable.getValue());
            return store;
        }, 
        null, null)
        .setTableRefreshBinding(DatamodelPackage.Literals.ASSET__ID, DatamodelPackage.Literals.ASSET__NAME, DatamodelPackage.Literals.ASSET__NODE)
        .setAfterCreateTableElementAction(tableView -> {
            tableView.addColumn("ID", 100, Store::getId);
            tableView.addColumn("Name", 100, p -> p == null ? "" : p.getName());
            tableView.addColumn("Node", 100, Store::getNode, p -> p == null ? "" : p.getName());
        });

new ObjectsPage<Truck>(this, DatamodelPackage.Literals.SCENARIO__TRUCKS, TreeElementType.TRUCK, 
        () -> {
            var truck = DatamodelFactory.eINSTANCE.createTruck();
            truck.setScenario(scenarioObservable.getValue());
            return truck;
        },
        null, null)
        .setTableRefreshBinding(DatamodelPackage.Literals.TRUCK__ID, DatamodelPackage.Literals.TRUCK__NAME,
                DatamodelPackage.Literals.TRUCK__SPEED, DatamodelPackage.Literals.TRUCK__INITIAL_NODE)
        .setAfterCreateTableElementAction(tableView -> {
            tableView.addColumn("ID", 100, Truck::getId);
            tableView.addColumn("Name", 100, p -> p == null ? "" : p.getName());
            tableView.addColumn("Speed", 100, Truck::getSpeed);
            tableView.addColumn("Initial node", 100, Truck::getInitialNode, p -> p == null ? "" : p.getName());
        });

When we add new nodes and arcs, their number is updated in the scenario tree. To do the same object count automatic update for assets and trucks, rewrite the TreePart.initializeDependentScenarioListFields() method:

TreePart.java, updated initializeDependentScenarioListFields() method
private void initializeDependentScenarioListFields(List<EStructuralFeature> list) {
    list.add(DatamodelPackage.Literals.SCENARIO__NODES);
    list.add(DatamodelPackage.Literals.SCENARIO__ARCS);
    list.add(DatamodelPackage.Literals.SCENARIO__WAREHOUSES);
    list.add(DatamodelPackage.Literals.SCENARIO__STORES);
    list.add(DatamodelPackage.Literals.SCENARIO__TRUCKS);
}

Add property pages

In general, to edit the scenario elements represented by the new scenario tree items, we need to do the following:

  • handle scenario tree item selection - to show a list of assets or trucks, in the same way as the lists of arcs and nodes are displayed;

  • add property pages for assets and trucks and show them when the respective entity is selected.

Property pages are located in the …​application.pages package. You have seen and updated one of those pages already - the ScenarioPage.

Add three more page classes to that package:

WarehousePage.java
package com.company.tutorial3.application.pages;

import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.emf.databinding.EMFProperties;
import org.eclipse.emf.databinding.FeaturePath;

import com.amalgamasimulation.desktop.binding.UpdateValueStrategyFactory;
import com.company.tutorial3.common.localization.Messages;
import com.company.tutorial3.datamodel.DatamodelPackage;
import com.company.tutorial3.datamodel.Node;
import com.company.tutorial3.datamodel.Warehouse;

public class WarehousePage extends AbstractPage<Warehouse> {

    @SuppressWarnings("all")
    private IObservableList<Node> nodeListObservable = EMFProperties.list(DatamodelPackage.Literals.SCENARIO__NODES)
            .observeDetail(scenarioObservable);

    public WarehousePage(Messages messages) {
        super(messages, Warehouse::getScenario);
    }

    @Override
    public boolean isVisible(Object selectedObject) {
        return selectedObject instanceof Warehouse;
    }

    @Override
    protected String getNameClassObject() {
        return "Warehouse";
    }

    @Override
    protected String getObjectDisplayName() {
        return observable.getValue().getId() + " - " + observable.getValue().getName();
    }

    @Override
    protected final FeaturePath[] getUpdateListeners() {
        return new FeaturePath[] { 
                FeaturePath.fromList(DatamodelPackage.Literals.ASSET__ID),
                FeaturePath.fromList(DatamodelPackage.Literals.ASSET__NAME) };
    }

    @Override
    protected void createControlsInternal() {
        addStringSection("ID", DatamodelPackage.Literals.ASSET__ID)
                .addTextbox(UpdateValueStrategyFactory.stringIsNotEmpty());
        addStringSection("Name", DatamodelPackage.Literals.ASSET__NAME)
                .addTextbox(UpdateValueStrategyFactory.stringIsNotEmpty());
        addReferenceSection("Node", DatamodelPackage.Literals.ASSET__NODE)
                .addAutoCompleteTextbox(DatamodelPackage.Literals.NODE__NAME, nodeListObservable)
                .addSelectionDialogButton("a node", nodeListObservable, tableView -> {
                    tableView.addColumn("Name", 150, Node::getName);
                }).addClearButton().setTextFieldCanBeEmpty(false);
    }
}
StorePage.java
package com.company.tutorial3.application.pages;

import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.emf.databinding.EMFProperties;
import org.eclipse.emf.databinding.FeaturePath;

import com.amalgamasimulation.desktop.binding.UpdateValueStrategyFactory;
import com.company.tutorial3.common.localization.Messages;
import com.company.tutorial3.datamodel.DatamodelPackage;
import com.company.tutorial3.datamodel.Node;
import com.company.tutorial3.datamodel.Store;

public class StorePage extends AbstractPage<Store> {

    @SuppressWarnings("all")
    private IObservableList<Node> nodeListObservable = EMFProperties.list(DatamodelPackage.Literals.SCENARIO__NODES)
            .observeDetail(scenarioObservable);

    public StorePage(Messages messages) {
        super(messages, Store::getScenario);
    }

    @Override
    public boolean isVisible(Object selectedObject) {
        return selectedObject instanceof Store;
    }

    @Override
    protected String getNameClassObject() {
        return "Store";
    }

    @Override
    protected String getObjectDisplayName() {
        return observable.getValue().getId() + " - " + observable.getValue().getName();
    }

    @Override
    protected final FeaturePath[] getUpdateListeners() {
        return new FeaturePath[] {
                FeaturePath.fromList(DatamodelPackage.Literals.ASSET__ID),
                FeaturePath.fromList(DatamodelPackage.Literals.ASSET__NAME) };
    }

    @Override
    protected void createControlsInternal() {
        addStringSection("ID", DatamodelPackage.Literals.ASSET__ID)
                .addTextbox(UpdateValueStrategyFactory.stringIsNotEmpty());
        addStringSection("Name", DatamodelPackage.Literals.ASSET__NAME)
                .addTextbox(UpdateValueStrategyFactory.stringIsNotEmpty());
        addReferenceSection("Node", DatamodelPackage.Literals.ASSET__NODE)
                .addAutoCompleteTextbox(DatamodelPackage.Literals.NODE__NAME, nodeListObservable)
                .addSelectionDialogButton("a node", nodeListObservable, tableView -> {
                    tableView.addColumn("Name", 150, Node::getName);
                }).addClearButton().setTextFieldCanBeEmpty(false);
    }
}
TruckPage.java
package com.company.tutorial3.application.pages;

import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.emf.databinding.EMFProperties;
import org.eclipse.emf.databinding.FeaturePath;

import com.amalgamasimulation.desktop.binding.UpdateValueStrategyFactory;
import com.company.tutorial3.common.localization.Messages;
import com.company.tutorial3.datamodel.DatamodelPackage;
import com.company.tutorial3.datamodel.Node;
import com.company.tutorial3.datamodel.Truck;

public class TruckPage extends AbstractPage<Truck> {

    @SuppressWarnings("all")
    private IObservableList<Node> nodeListObservable = EMFProperties.list(DatamodelPackage.Literals.SCENARIO__NODES)
            .observeDetail(scenarioObservable);

    public TruckPage(Messages messages) {
        super(messages, Truck::getScenario);
    }

    @Override
    public boolean isVisible(Object selectedObject) {
        return selectedObject instanceof Truck;
    }

    @Override
    protected String getNameClassObject() {
        return "Truck";
    }

    @Override
    protected String getObjectDisplayName() {
        return observable.getValue().getId() + " - " + observable.getValue().getName();
    }

    @Override
    protected final FeaturePath[] getUpdateListeners() {
        return new FeaturePath[] {
                FeaturePath.fromList(DatamodelPackage.Literals.TRUCK__ID),
                FeaturePath.fromList(DatamodelPackage.Literals.TRUCK__NAME) };
    }

    @Override
    protected void createControlsInternal() {
        addStringSection("ID", DatamodelPackage.Literals.TRUCK__ID)
                .addTextbox(UpdateValueStrategyFactory.stringIsNotEmpty());
        addStringSection("Name", DatamodelPackage.Literals.TRUCK__NAME)
                .addTextbox(UpdateValueStrategyFactory.stringIsNotEmpty());
        addNumericSection("Speed", DatamodelPackage.Literals.TRUCK__SPEED)
                .addTextbox(UpdateValueStrategyFactory.doublePositive());
        addReferenceSection("Initial node", DatamodelPackage.Literals.TRUCK__INITIAL_NODE)
                .addAutoCompleteTextbox(DatamodelPackage.Literals.NODE__NAME, nodeListObservable)
                .addSelectionDialogButton("a node", nodeListObservable, tableView -> {
                    tableView.addColumn("Name", 150, Node::getName);
                }).addClearButton().setTextFieldCanBeEmpty(false);
    }
}

Locate the PropertiesPart class in the …​application.parts.editor package. Update its registerPages() method (and add appropriate import statements):

PropertiesPart.java
@Override
protected void registerPages() {
    registerPage(new ScenarioPage (messages), messages.tab_general);
    registerPage(new NodePage(messages), messages.tab_general);
    registerPage(new ArcPage(messages), messages.tab_general);
    registerPage(new PointPage(messages), messages.tab_bendpoint);
    registerPage(new WarehousePage(messages), messages.tab_general);
    registerPage(new StorePage(messages), messages.tab_general);
    registerPage(new TruckPage(messages), messages.tab_general);
}

This registers new property pages that will shown the detailed (and editable) object information in the bottom right part of the application window when an object (a truck, etc.) is selected in the bottom left part.

Check the result

Start the application and create a new scenario. Use the editor mode to populate the new scenario with some data:

  1. Add several Nodes.

  2. Add several Warehouses and several Stores. Give them unique IDs and make their names equal to their IDs.

  3. Add several Trucks, place them to different nodes.

Each time you add a node, an asset or a truck, its quantity displayed in the scenario tree should be updated.