org.eclipse.sirius.diagram.ui.internal.operation.RegionContainerUpdateLayoutOperation.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.sirius.diagram.ui.internal.operation.RegionContainerUpdateLayoutOperation.java

Source

/*******************************************************************************
 * Copyright (c) 2013, 2016 THALES GLOBAL SERVICES and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Obeo - initial API and implementation
 *******************************************************************************/
package org.eclipse.sirius.diagram.ui.internal.operation;

import java.io.File;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Path;
import org.eclipse.draw2d.FigureUtilities;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.edit.provider.IItemLabelProvider;
import org.eclipse.emf.edit.ui.provider.ExtendedImageRegistry;
import org.eclipse.gmf.runtime.notation.LayoutConstraint;
import org.eclipse.gmf.runtime.notation.Location;
import org.eclipse.gmf.runtime.notation.Node;
import org.eclipse.gmf.runtime.notation.NotationFactory;
import org.eclipse.gmf.runtime.notation.NotationPackage;
import org.eclipse.gmf.runtime.notation.Size;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.sirius.common.tools.api.resource.FileProvider;
import org.eclipse.sirius.common.tools.api.util.StringUtil;
import org.eclipse.sirius.diagram.BorderedStyle;
import org.eclipse.sirius.diagram.DNodeContainer;
import org.eclipse.sirius.diagram.WorkspaceImage;
import org.eclipse.sirius.diagram.business.api.query.DDiagramElementQuery;
import org.eclipse.sirius.diagram.business.internal.query.DDiagramElementContainerExperimentalQuery;
import org.eclipse.sirius.diagram.business.internal.query.DNodeContainerExperimentalQuery;
import org.eclipse.sirius.diagram.ui.business.api.query.ViewQuery;
import org.eclipse.sirius.diagram.ui.business.api.view.SiriusGMFHelper;
import org.eclipse.sirius.diagram.ui.business.internal.operation.AbstractModelChangeOperation;
import org.eclipse.sirius.diagram.ui.edit.api.part.AbstractDiagramElementContainerEditPart;
import org.eclipse.sirius.diagram.ui.internal.edit.parts.DNodeContainerViewNodeContainerCompartment2EditPart;
import org.eclipse.sirius.diagram.ui.internal.edit.parts.DNodeContainerViewNodeContainerCompartmentEditPart;
import org.eclipse.sirius.diagram.ui.internal.refresh.GMFHelper;
import org.eclipse.sirius.diagram.ui.part.SiriusVisualIDRegistry;
import org.eclipse.sirius.diagram.ui.provider.DiagramUIPlugin;
import org.eclipse.sirius.diagram.ui.provider.Messages;
import org.eclipse.sirius.diagram.ui.tools.api.figure.AlphaDropShadowBorder;
import org.eclipse.sirius.diagram.ui.tools.api.graphical.edit.styles.IContainerLabelOffsets;
import org.eclipse.sirius.ui.tools.api.color.VisualBindingManager;
import org.eclipse.sirius.viewpoint.BasicLabelStyle;
import org.eclipse.sirius.viewpoint.DRepresentationElement;
import org.eclipse.sirius.viewpoint.Style;
import org.eclipse.sirius.viewpoint.description.RepresentationElementMapping;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;

import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;

/**
 * Update and keep consistent the GMF Bounds of a regions container and its
 * regions.
 * 
 * @author mporhel
 */
public class RegionContainerUpdateLayoutOperation extends AbstractModelChangeOperation<Void> {

    private final Node regionsContainer;

    /**
     * True if this layout operation is caused for a regions container that has
     * at least one new region or one region less, false otherwise.
     */
    private final boolean containsCreatedOrDeletedRegions;

    /**
     * Constructor.
     * 
     * @param regionsContainer
     *            The regions container view to layout.
     */
    public RegionContainerUpdateLayoutOperation(Node regionsContainer) {
        super(Messages.RegionContainerUpdateLayoutOperation_name);
        this.regionsContainer = extractRealRegionsContainer(regionsContainer);
        this.containsCreatedOrDeletedRegions = false;
    }

    /**
     * Constructor.
     * 
     * @param regionsContainer
     *            The regions container view to layout.
     * @param containsCreatedOrDeletedRegions
     *            true if this layout operation is caused for a regions
     *            container that has at least one new region or one region less.
     */
    public RegionContainerUpdateLayoutOperation(Node regionsContainer, boolean containsCreatedOrDeletedRegions) {
        super(Messages.RegionContainerUpdateLayoutOperation_name);
        this.regionsContainer = extractRealRegionsContainer(regionsContainer);
        this.containsCreatedOrDeletedRegions = containsCreatedOrDeletedRegions;
    }

    private Node extractRealRegionsContainer(Node node) {
        if (node != null && node.eContainer() instanceof Node) {
            int visualID = SiriusVisualIDRegistry.getVisualID(node);
            if (DNodeContainerViewNodeContainerCompartmentEditPart.VISUAL_ID == visualID
                    || DNodeContainerViewNodeContainerCompartment2EditPart.VISUAL_ID == visualID) {
                return (Node) node.eContainer();
            }
        }
        return node;
    }

    private List<Node> getRegionsToLayout() {
        List<Node> regionsToLayout = Lists.newArrayList();
        if (regionsContainer != null) {
            Node labelNode = SiriusGMFHelper.getLabelNode(regionsContainer);
            List<Node> nodes = Lists.newArrayList(Iterables.filter(regionsContainer.getChildren(), Node.class));
            if (labelNode != null && nodes.size() == 2) {
                nodes.remove(labelNode);
                Node compartment = nodes.iterator().next();
                Iterables.addAll(regionsToLayout, Iterables.filter(compartment.getChildren(), Node.class));
            }
        }
        return regionsToLayout;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Void execute() {
        List<Node> regionsToLayout = getRegionsToLayout();
        if (!regionsToLayout.isEmpty()) {
            EObject element = regionsContainer.getElement();
            if (element instanceof DNodeContainer) {
                DNodeContainer dnc = (DNodeContainer) element;
                List<Node> regionsToLayoutSortedByLocation = Lists.newArrayList(regionsToLayout);
                sortRegionsAccordingToLocation(dnc, regionsToLayoutSortedByLocation);
                sortRegions(dnc, regionsToLayout);
                // Check if the order from location (x axis for horizontal stack
                // and y for vertical) is the same as the stored order. If not a
                // layout must be launched.
                boolean isSameOrder = regionsToLayout.equals(regionsToLayoutSortedByLocation);
                if (!isSameOrder || containsCreatedOrDeletedRegions) {
                    DNodeContainerExperimentalQuery query = new DNodeContainerExperimentalQuery(dnc);
                    if (query.isVerticalStackContainer()) {
                        updateRegionsLayoutContraints(regionsToLayout, true,
                                new DDiagramElementContainerExperimentalQuery(dnc).isRegion());
                    } else if (query.isHorizontaltackContainer()) {
                        updateRegionsLayoutContraints(regionsToLayout, false,
                                new DDiagramElementContainerExperimentalQuery(dnc).isRegion());
                    }
                }
            }
        }
        return null;
    }

    /*
     * @param isVertical : vertical if true, horizontal if false.
     */
    private void updateRegionsLayoutContraints(List<Node> regionsToLayout, boolean isVertical,
            boolean containerIsRegion) {
        Size regionContainerSize = null;
        LayoutConstraint regionContainerLayoutConstraint = regionsContainer.getLayoutConstraint();
        if (regionContainerLayoutConstraint instanceof Size) {
            regionContainerSize = (Size) regionContainerLayoutConstraint;
        } else {
            regionContainerSize = NotationFactory.eINSTANCE.createSize();
        }

        // The first item of this list is the default size of a region and a the
        // second (optional) is the size of the last region.
        List<Dimension> defaultRegionsSizeAndOptionalLastRegionSize = null;
        Dimension regionsSizeComputedFromContainer = new Dimension(-1, -1);
        // The regionsSizeComputedFromContainer is useful only when the
        // regionContainer size has been fixed, by VSM or
        // by end-user at creation.
        if (hasRegionContainerFixedSize(regionContainerSize, isVertical)) {
            defaultRegionsSizeAndOptionalLastRegionSize = computeRegionsSizeAccordingToContainerSize(
                    regionsToLayout.size(), isVertical, containerIsRegion, regionContainerSize);
            regionsSizeComputedFromContainer = defaultRegionsSizeAndOptionalLastRegionSize.get(0);
        }

        // The current bounds of the regions.
        Map<Node, Rectangle> regionsBounds = Maps.newHashMap();
        for (Node node : regionsToLayout) {
            Rectangle bounds = GMFHelper.getBounds(node, true, true);
            regionsBounds.put(node, bounds);
        }
        // The default region size is computed according to the VSM defined size
        // of the corresponding mapping and to the regions size computed from
        // container size. It is then used according to stack orientation.
        Dimension defaultRegionsSize = getDefaultRegionsSize(regionsBounds, regionsSizeComputedFromContainer);

        int y = 0;
        int x = 0;
        for (Node node : regionsToLayout) {
            Rectangle bounds = regionsBounds.get(node);

            LayoutConstraint layoutConstraint = node.getLayoutConstraint();
            if (layoutConstraint instanceof Location) {
                Location loc = (Location) layoutConstraint;
                if (loc.getX() != x) {
                    loc.setX(x);
                }
                if (loc.getY() != y) {
                    loc.setY(y);
                }
            }

            if (layoutConstraint instanceof Size) {
                Size size = (Size) layoutConstraint;
                if (isVertical) {
                    if ((size.getWidth() != -1 || regionsSizeComputedFromContainer.width() != -1)
                            && size.getWidth() != defaultRegionsSize.width()) {
                        size.setWidth(defaultRegionsSize.width());
                    }
                    if (regionsSizeComputedFromContainer.height() != -1) {
                        if (node.equals(regionsToLayout.get(regionsToLayout.size() - 1))
                                && defaultRegionsSizeAndOptionalLastRegionSize != null
                                && defaultRegionsSizeAndOptionalLastRegionSize.size() == 2) {
                            // Specific height for last region
                            bounds.height = defaultRegionsSizeAndOptionalLastRegionSize.get(1).height();
                        } else {
                            bounds.height = defaultRegionsSize.height();
                        }
                        size.setHeight(bounds.height);
                    }
                } else {
                    if ((size.getHeight() != -1 || regionsSizeComputedFromContainer.height() != -1)
                            && size.getHeight() != defaultRegionsSize.height()) {
                        size.setHeight(defaultRegionsSize.height());
                    }
                    if (regionsSizeComputedFromContainer.width() != -1) {
                        if (node.equals(regionsToLayout.get(regionsToLayout.size() - 1))
                                && defaultRegionsSizeAndOptionalLastRegionSize != null
                                && defaultRegionsSizeAndOptionalLastRegionSize.size() == 2) {
                            // Specific width for last region
                            bounds.width = defaultRegionsSizeAndOptionalLastRegionSize.get(1).width();
                        } else {
                            bounds.width = defaultRegionsSize.width();
                        }
                        size.setWidth(bounds.width);
                    }
                }
            }

            if (isVertical) {
                y += bounds.height;
            } else {
                x += bounds.width;
            }

        }

        // Set the width and height of the container to -1 as the size is
        // managed by the region contained in this container.
        if (isVertical) {
            if (regionContainerSize.getHeight() != -1) {
                regionContainerSize.setHeight(-1);
            }
            if (regionContainerSize.getWidth() != -1) {
                regionContainerSize.setWidth(-1);
            }
        } else {
            if (regionContainerSize.getWidth() != -1) {
                regionContainerSize.setWidth(-1);
            }
            if (regionContainerSize.getHeight() != -1) {
                regionContainerSize.setHeight(-1);
            }
        }
    }

    /**
     * Check if the current regions container size is a fixed size (fixed in the
     * VSM or by the end-user):
     * <ul>
     * <li>If it is fixed by the end-user both width and height will be
     * different than -1.</li>
     * <li>If it is fixed in the VSM, it is possible that only one value is
     * different that -1, but in this case, only one size is interesting
     * according to stack orietation.</li>
     * </ul>
     * 
     * @param regionsContainerSize
     *            The GMF size of the current regions container
     * @param isVertical
     *            the stack orientation (true for vertical, false for
     *            horizontal)
     * @return true if the <code>regionsContainerSize</code> is considered as
     *         fixed size, false otherwise.
     */
    private boolean hasRegionContainerFixedSize(Size regionsContainerSize, boolean isVertical) {
        boolean result = regionsContainerSize.getWidth() != -1 && regionsContainerSize.getHeight() != -1;
        if (!result && (regionsContainerSize.getWidth() != -1 || regionsContainerSize.getHeight() != -1)) {
            EObject element = regionsContainer.getElement();
            if (element instanceof DNodeContainer) {
                DNodeContainer dnc = (DNodeContainer) element;
                if (isVertical && regionsContainerSize.getHeight() != -1) {
                    result = regionsContainerSize.getHeight() == dnc.getHeight();
                } else if (!isVertical && regionsContainerSize.getWidth() != -1) {
                    result = regionsContainerSize.getWidth() == dnc.getWidth();
                }
            }
        }
        return result;
    }

    /**
     * This method computes the default region size according to all bounds of
     * the region and to the <code>regionSizeComputedFromContainer</code>.
     * 
     * @param regionsBounds
     *            The bounds of the regions, of the current regions container,
     *            to layout.
     * @param regionSizeComputedFromContainer
     *            The default region size computed from regions container size
     *            (if it is defined). If the regions container size is not
     *            defined, the <code>regionSizeComputedFromContainer</code>
     *            width/height will be "-1". If it is not "-1", it is priority.
     * @return The default dimension to consider for the region of this regions
     *         container.
     */
    private Dimension getDefaultRegionsSize(Map<Node, Rectangle> regionsBounds,
            Dimension regionSizeComputedFromContainer) {
        Dimension result = new Dimension(-1, -1);
        for (Rectangle bounds : regionsBounds.values()) {
            result.width = Math.max(result.width, bounds.width);
            result.height = Math.max(result.height, bounds.height);
        }
        if (regionSizeComputedFromContainer.width() != -1) {
            // The size of the regions container has been fixed, by VSM or
            // by end-user at creation, this dimension must be kept for the
            // first region creation. At the next pass in this method, the
            // size of the regions container will be {-1, -1}: this is done
            // at the end of the current layout operation.
            result.width = regionSizeComputedFromContainer.width();
        }
        if (regionSizeComputedFromContainer.height() != -1) {
            // The size of the regions container has been fixed, by VSM or
            // by end-user at creation, this dimension must be kept for the
            // first region creation. At the next pass in this method, the
            // size of the regions container will be {-1, -1}: this is done
            // at the end of the current layout operation.
            result.height = regionSizeComputedFromContainer.height();
        }
        return result;
    }

    /**
     * Compute the default size of regions according to regions container size.
     * This method is called only when the regions container has not an auto
     * sized for its GMF size.
     * 
     * @param nbRegionsToLayout
     *            Number of regions to layout
     * @param vertical
     *            true if the stack is vertical, false if it is horizontal
     * @param containerIsRegion
     *            true if the container is also a region
     * @param regionsContainerSize
     *            The size, defined in the VSM or by the end-user at creation,
     *            of the regions container
     * @return list of dimension (the first is the size of a region, the second,
     *         optional, is the size of the last region if different of the
     *         others)
     */
    private List<Dimension> computeRegionsSizeAccordingToContainerSize(int nbRegionsToLayout, boolean vertical,
            boolean containerIsRegion, Size regionsContainerSize) {
        List<Dimension> result = Lists.newArrayList();
        Dimension regionContainerContentPaneSize = new Dimension(regionsContainerSize.getWidth(),
                regionsContainerSize.getHeight());
        Dimension regionSize = new Dimension();
        // headerHeight includes the title height (with the icon), the label
        // offset, the 1 pixel separator line,
        int headerHeight = 0;
        EObject element = regionsContainer.getElement();
        ViewQuery viewQuery = new ViewQuery(regionsContainer);
        int borderSize = 0;
        boolean needShadowBorder = true;
        if (element instanceof DNodeContainer) {
            DNodeContainer dnc = (DNodeContainer) element;
            Style siriusStyle = dnc.getStyle();
            // Get border size
            if (siriusStyle instanceof BorderedStyle) {
                if (((BorderedStyle) siriusStyle).getBorderSize() != null) {
                    borderSize = ((BorderedStyle) siriusStyle).getBorderSize().intValue();
                }
            }
            // Get header size (title and all associated margin)
            if (!new DDiagramElementQuery(dnc).isLabelHidden()) {
                int titleHeight = 0;
                if (siriusStyle instanceof BasicLabelStyle) {
                    BasicLabelStyle bls = (BasicLabelStyle) siriusStyle;
                    Font defaultFont = VisualBindingManager.getDefault().getFontFromLabelStyle(bls,
                            (String) viewQuery.getDefaultValue(NotationPackage.Literals.FONT_STYLE__FONT_NAME));
                    Dimension titleDimension;
                    try {
                        titleDimension = FigureUtilities.getStringExtents(dnc.getName(), defaultFont);
                    } catch (SWTException e) {
                        // Probably an "Invalid thread access" (FigureUtilities
                        // creates a new Shell to compute the label size). So in
                        // this case, we use a default size.
                        titleDimension = new Dimension(20, 12);
                    }
                    titleHeight = titleDimension.height;
                    if (bls.isShowIcon()) {
                        // Also consider the icon size
                        Image icon = null;
                        ImageDescriptor descriptor = null;
                        EObject target = dnc.getTarget();
                        if (!StringUtil.isEmpty(bls.getIconPath())) {
                            String iconPath = bls.getIconPath();
                            final File imageFile = FileProvider.getDefault().getFile(new Path(iconPath));
                            if (imageFile != null && imageFile.exists() && imageFile.canRead()) {
                                try {
                                    descriptor = DiagramUIPlugin.Implementation
                                            .findImageDescriptor(imageFile.toURI().toURL());
                                } catch (MalformedURLException e) {
                                    // Do nothing here
                                }
                            }
                        } else if (target != null) {
                            final IItemLabelProvider labelProvider = (IItemLabelProvider) DiagramUIPlugin
                                    .getPlugin().getItemProvidersAdapterFactory()
                                    .adapt(target, IItemLabelProvider.class);
                            if (labelProvider != null) {
                                descriptor = ExtendedImageRegistry.getInstance()
                                        .getImageDescriptor(labelProvider.getImage(target));
                            }
                        }

                        if (descriptor == null) {
                            descriptor = ImageDescriptor.getMissingImageDescriptor();
                        }
                        icon = DiagramUIPlugin.getPlugin().getImage(descriptor);
                        titleHeight = Math.max(titleHeight, icon.getBounds().height);
                    }
                }
                int separatorLineHeight = 1;
                int labelOffset = IContainerLabelOffsets.LABEL_OFFSET;
                // As in
                // org.eclipse.sirius.diagram.ui.edit.internal.part.DiagramContainerEditPartOperation.refreshBorder(AbstractDiagramElementContainerEditPart,
                // ViewNodeContainerFigureDesc, ContainerStyle)
                if (!containerIsRegion) {
                    labelOffset -= 1;
                }
                headerHeight = labelOffset + titleHeight + AbstractDiagramElementContainerEditPart.DEFAULT_SPACING
                        + separatorLineHeight;
            }
            // Compute if the shadow border is needed (as in
            // AbstractDiagramElementContainerEditPart.addDropShadow(NodeFigure,
            // IFigure))
            needShadowBorder = !(new DDiagramElementContainerExperimentalQuery(dnc).isRegion()
                    || dnc.getOwnedStyle() instanceof WorkspaceImage);
        }

        // Adds all the expected margin
        regionContainerContentPaneSize
                .setHeight(regionContainerContentPaneSize.height() - (borderSize + headerHeight + borderSize));
        regionContainerContentPaneSize.setWidth(regionContainerContentPaneSize.width() - (borderSize + borderSize));
        if (needShadowBorder) {
            // Remove the 2 pixels corresponding to the shadow insets
            // (AlphaDropShadowBorder.getInsets(IFigure))
            regionContainerContentPaneSize.setHeight(
                    regionContainerContentPaneSize.height() - AlphaDropShadowBorder.getDefaultShadowSize());
            regionContainerContentPaneSize.setWidth(
                    regionContainerContentPaneSize.width() - AlphaDropShadowBorder.getDefaultShadowSize());
        }
        if (vertical) {
            if (regionContainerContentPaneSize.height() != 0) {
                regionSize.setHeight(regionContainerContentPaneSize.height() / nbRegionsToLayout);
            }
            regionSize.setWidth(regionContainerContentPaneSize.width());
            result.add(regionSize);
            if (regionContainerContentPaneSize.height() % regionSize.height() != 0) {
                // Compute the precise last height (the regions container height
                // is not necessarily exactly divisible by the number of
                // regions)
                int lastRegionHeight = regionContainerContentPaneSize.height()
                        - (regionSize.height() * (nbRegionsToLayout - 1));
                result.add(new Dimension(regionContainerContentPaneSize.width(), lastRegionHeight));
            }
        } else {
            if (regionsContainerSize.getWidth() != 0) {
                regionSize.setWidth(regionContainerContentPaneSize.width() / nbRegionsToLayout);
            }
            regionSize.setHeight(regionContainerContentPaneSize.height());
            result.add(regionSize);
            if (regionContainerContentPaneSize.width() % regionSize.width() != 0) {
                // Compute the precise last width (the regions container width
                // is not necessarily exactly divisible by the number of
                // regions)
                int lastRegionWidth = regionContainerContentPaneSize.width()
                        - (regionSize.width() * (nbRegionsToLayout - 1));
                result.add(new Dimension(lastRegionWidth, regionContainerContentPaneSize.height()));
            }
        }
        return result;
    }

    /**
     * Sort the regions with the ddiagram element index comparator and then with
     * a mapping comparator.
     * 
     * @param dNodeContainer
     *            the {@link DNodeContainer} regions container
     * @param modelChildren
     *            the regions
     */
    public static void sortRegions(DNodeContainer dNodeContainer, List<? extends View> modelChildren) {
        new RegionComparisonHelper(dNodeContainer).sort(modelChildren);
    }

    /**
     * Sort the regions with the current x or y location.
     * 
     * @param dNodeContainer
     *            the {@link DNodeContainer} regions container
     * @param modelChildren
     *            the regions
     */
    public static void sortRegionsAccordingToLocation(DNodeContainer dNodeContainer,
            List<? extends View> modelChildren) {
        final DNodeContainerExperimentalQuery query = new DNodeContainerExperimentalQuery(dNodeContainer);
        Function<View, Integer> locationIndex = new Function<View, Integer>() {
            @Override
            public Integer apply(View view) {
                if (view instanceof Node) {
                    LayoutConstraint constraint = ((Node) view).getLayoutConstraint();
                    if (constraint instanceof Location) {
                        if (query.isHorizontaltackContainer()) {
                            return ((Location) constraint).getX();
                        } else if (query.isVerticalStackContainer()) {
                            return ((Location) constraint).getY();
                        }
                    }
                }
                return Integer.MAX_VALUE;
            }
        };
        Collections.sort(modelChildren, Ordering.natural().onResultOf(locationIndex));
    }

    private static class RegionComparisonHelper extends ComparisonHelper {
        private DNodeContainer self;

        public RegionComparisonHelper(DNodeContainer self) {
            this.self = self;
        }

        @Override
        protected List<? extends DRepresentationElement> getDElementsToSort() {
            return self.getOwnedDiagramElements();
        }

        @Override
        protected List<? extends RepresentationElementMapping> getMappingsToSort() {
            return self.getActualMapping().getAllContainerMappings();
        }
    }
}