/********************************************** * Copyright (C) 2010 Lukas Laag * This file is part of svgreal. * * svgreal is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * svgreal is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with svgreal. If not, see http://www.gnu.org/licenses/ **********************************************/ package org.vectomatic.svg.edit.client.engine; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import org.vectomatic.dom.svg.OMElement; import org.vectomatic.dom.svg.OMSVGDocument; import org.vectomatic.dom.svg.OMSVGElement; import org.vectomatic.dom.svg.OMSVGGElement; import org.vectomatic.dom.svg.OMSVGMatrix; import org.vectomatic.dom.svg.OMSVGPoint; import org.vectomatic.dom.svg.OMSVGRect; import org.vectomatic.dom.svg.OMSVGRectElement; import org.vectomatic.dom.svg.OMSVGSVGElement; import org.vectomatic.dom.svg.OMSVGStyle; import org.vectomatic.dom.svg.impl.SVGElement; import org.vectomatic.dom.svg.impl.SVGGElement; import org.vectomatic.dom.svg.impl.SVGRectElement; import org.vectomatic.dom.svg.itf.ISVGLocatable; import org.vectomatic.dom.svg.itf.ISVGTransformable; import org.vectomatic.dom.svg.utils.DOMHelper; import org.vectomatic.dom.svg.utils.OMSVGParser; import org.vectomatic.dom.svg.utils.SVGConstants; import org.vectomatic.dom.svg.utils.SVGPrefixResolver; import org.vectomatic.svg.edit.client.SVGSelectionModel; import org.vectomatic.svg.edit.client.SvgrealApp; import org.vectomatic.svg.edit.client.command.CommandStore; import org.vectomatic.svg.edit.client.command.ICommandFactory; import org.vectomatic.svg.edit.client.command.RemoveElementsCommandFactory; import org.vectomatic.svg.edit.client.command.ShowPropertiesCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddCircleCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddEllipseCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddGroupCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddLineCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddPathCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddPolygonCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddPolylineCommandFactory; import org.vectomatic.svg.edit.client.command.add.AddRectCommandFactory; import org.vectomatic.svg.edit.client.event.HasRotationHandlers; import org.vectomatic.svg.edit.client.event.HasScalingHandlers; import org.vectomatic.svg.edit.client.event.KeyPressProcessor; import org.vectomatic.svg.edit.client.event.KeyUpProcessor; import org.vectomatic.svg.edit.client.event.MouseDownProcessor; import org.vectomatic.svg.edit.client.event.MouseMoveProcessor; import org.vectomatic.svg.edit.client.event.MouseUpProcessor; import org.vectomatic.svg.edit.client.event.RotationEvent; import org.vectomatic.svg.edit.client.event.RotationHandler; import org.vectomatic.svg.edit.client.event.ScalingEvent; import org.vectomatic.svg.edit.client.event.ScalingHandler; import org.vectomatic.svg.edit.client.event.StoreEventProcessor; import org.vectomatic.svg.edit.client.gxt.widget.CommandFactoryMenuItem; import org.vectomatic.svg.edit.client.gxt.widget.KeyNavExt; import org.vectomatic.svg.edit.client.model.MetaModel; import org.vectomatic.svg.edit.client.model.svg.SVGCircleElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGEllipseElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGImageElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGLineElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGNamedElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGPathElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGPolygonElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGPolylineElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGRectElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGUseElementModel; import org.vectomatic.svg.edit.client.model.svg.SVGViewBoxElementModel; import com.extjs.gxt.ui.client.event.ComponentEvent; import com.extjs.gxt.ui.client.store.StoreEvent; import com.extjs.gxt.ui.client.store.StoreListener; import com.extjs.gxt.ui.client.store.TreeStore; import com.extjs.gxt.ui.client.widget.menu.Item; import com.extjs.gxt.ui.client.widget.menu.Menu; import com.extjs.gxt.ui.client.widget.treepanel.TreePanelSelectionModel; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.event.dom.client.MouseEvent; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseMoveHandler; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.dom.client.MouseUpHandler; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; /** * Model class for an SVG document edited by the application. * The document has the following DOM structure: * <pre> * <svg> * <defs> * <g> <!-- xform group : xforms applies here --> * <g/> <!-- grid group --> * <g> <!-- element group : opacity applies here ; root of the tree view (element part of the modelGroup) --> * <title/> * <desc/> * <rect/> <!-- viewbox --> * <circle/> ... <!-- geometry starts here --> * </g> * </g> * <g> <!-- twin group : xforms applies here; visiblity applies here (twin part of the modelGroup) --> * <title/> * <desc/> * <circle/> ... <!-- geometry starts here --> * </g> * </svg> * </pre> * <dl> * <dt>elementGroup</dt><dd>Contains all the elements from the original SVG, id-normalized.</dd> * <dt>twinGroup</dt><dd>Contains a visibility-hidden clone of the previous group. It is used to display the selection and implement highlighting.</dd> * <dt>gridGroup</dt><dd>Contains all the helper elements required to display grids, rulers...</dd> * <dt>geometryGroup</dt><dd>Used to applied opacity for highlighting operations</dd> * </dl> * @author laaglu */ public class SVGModel implements MouseDownHandler, MouseMoveHandler, MouseUpHandler, HasScalingHandlers, HasRotationHandlers { /** * To be able to identify the viewBox model */ private static final String ATTR_KIND = "kind"; private static final String ATTR_KIND_VIEWBOX = "viewBox"; /** * Id Prefix extension for twins */ public static final String EXT_TWIN = "twin"; /** * The prefix used for all id attributes in this model */ protected String idPrefix; /** * The root of the SVG document */ protected OMSVGSVGElement svg; /** * A group used to apply a visualization transform change the display * of the document. */ protected OMSVGGElement xformGroup; /** * The root of the model tree. */ protected SVGElementModel modelGroup; /** * The matrix transform to the xform group and twin group */ protected OMSVGMatrix m; /** * The current scaling the xform group and twin group */ protected float angle; /** * The current rotation of the xform group and twin group */ protected float scale; /** * The selection model */ protected SVGSelectionModel selectionModel; /** * The current mode (false = display mode, true = highlighting mode) */ protected boolean highlightingMode; /** * The highlighted model in highlighting mode */ protected SVGElementModel highlightedModel; /** * A map used to generate node names for nodes * which do not have a title element. */ protected Map<String, Integer> tagNameToTagCount; /** * The Store which contains this model data */ protected TreeStore<SVGElementModel> store; /** * The Store which contains this model commands */ protected CommandStore commandStore; /** * Associates SVG elements with their model wrapper */ protected Map<SVGElement, SVGElementModel> elementToModel; /** * Associates element ids with their model */ protected Map<String, Object> idToModel; /** * The svg rect use to represent the viewBox */ protected SVGViewBoxElementModel viewBox; /** * A rectangled defining the bounds of the GXT viewport */ protected OMSVGRect windowRect; /** * The grid settings for this model */ protected Grid grid; /*========================================================== * * C O N S T R U C T O R * *==========================================================*/ public SVGModel() { elementToModel = new HashMap<SVGElement, SVGElementModel>(); idToModel = new HashMap<String, Object>(); tagNameToTagCount = new HashMap<String, Integer>(); grid = new Grid(); } /*========================================================== * * M O D E L C O N S T R U C T I O N * *==========================================================*/ /** * Factory method. Creates a new SVG model from the supplied * SVG root and title * @param svg The svg root * @param title The svg title * @param idPrefix the id prefix for this model * @return The new SVG document */ public static SVGModel newInstance(OMSVGSVGElement svg, String title, String idPrefix) { SVGModel model = GWT.create(SVGModel.class); model.setSvgElement(svg, title, idPrefix); return model; } static { initialize(); } protected static Map<String, MetaModel<SVGElement>> tagNameToMetamodel; public static MetaModel<SVGElement> getMetamodel(SVGElement element) { if (tagNameToMetamodel == null) { tagNameToMetamodel = new HashMap<String, MetaModel<SVGElement>>(); tagNameToMetamodel.put(SVGConstants.SVG_CIRCLE_TAG, SVGCircleElementModel.getCircleElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_ELLIPSE_TAG, SVGEllipseElementModel.getEllipseElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_LINE_TAG, SVGLineElementModel.getLineElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_RECT_TAG, SVGRectElementModel.getRectElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_POLYGON_TAG, SVGPolygonElementModel.getPolygonElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_POLYLINE_TAG, SVGPolylineElementModel.getPolylineElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_PATH_TAG, SVGPathElementModel.getPathElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_IMAGE_TAG, SVGImageElementModel.getImageElementMetaModel()); tagNameToMetamodel.put(SVGConstants.SVG_USE_TAG, SVGUseElementModel.getUseElementMetaModel()); } return tagNameToMetamodel.get(element.getTagName()); } private static final native void initialize() /*-{ if ($wnd.otToModel == null) { $wnd.otToModel = new Object(); } $wnd.otToModel["SVGCircleElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGCircleElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGCircleElement;Lorg/vectomatic/dom/svg/impl/SVGCircleElement;)(owner, elem, twin); }; $wnd.otToModel["SVGEllipseElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGEllipseElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGEllipseElement;Lorg/vectomatic/dom/svg/impl/SVGEllipseElement;)(owner, elem, twin); }; $wnd.otToModel["SVGLineElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGLineElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGLineElement;Lorg/vectomatic/dom/svg/impl/SVGLineElement;)(owner, elem, twin); }; $wnd.otToModel["SVGRectElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGRectElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGRectElement;Lorg/vectomatic/dom/svg/impl/SVGRectElement;)(owner, elem, twin); }; $wnd.otToModel["SVGPolygonElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGPolygonElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGPolygonElement;Lorg/vectomatic/dom/svg/impl/SVGPolygonElement;)(owner, elem, twin); }; $wnd.otToModel["SVGPolylineElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGPolylineElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGPolylineElement;Lorg/vectomatic/dom/svg/impl/SVGPolylineElement;)(owner, elem, twin); }; $wnd.otToModel["SVGPathElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGPathElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGPathElement;Lorg/vectomatic/dom/svg/impl/SVGPathElement;)(owner, elem, twin); }; $wnd.otToModel["SVGImageElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGImageElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGImageElement;Lorg/vectomatic/dom/svg/impl/SVGImageElement;)(owner, elem, twin); }; $wnd.otToModel["SVGUseElement"] = function(owner, elem, twin) { return @org.vectomatic.svg.edit.client.model.svg.SVGUseElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGUseElement;Lorg/vectomatic/dom/svg/impl/SVGUseElement;)(owner, elem, twin); }; }-*/; /** * Generates a model around an overlay type node * @param <T> the node type * @param element The overlay type node * @return The node model */ public SVGElementModel convert(SVGElement element) { SVGElementModel model = elementToModel.get(element); // assert(model != null); return model; } public OMSVGSVGElement getSvgElement() { return svg; } /** * Binds this SVG model to the specified SVG 'svg' element * @param svg an SVG 'svg' element * @param title the name of the root element * @param idPrefix the id prefix for this model */ public void setSvgElement(OMSVGSVGElement svg, String title, String idPrefix) { this.svg = svg; this.idPrefix = idPrefix; windowRect = svg.createSVGRect(); // Force the svg to have its size managed by CSS // (no width and height attributes). This size will // be the min (window size, bbox of the svg in // screen coordinates taking into account the // viewing transform). svg.removeAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE); svg.removeAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE); // Add event handlers. These event handlers will re-route // events to the highlighter or the manipulators svg.addMouseMoveHandler(this); svg.addMouseDownHandler(this); svg.addMouseUpHandler(this); elementToModel.clear(); tagNameToTagCount.clear(); // Normalize ids to support multi-docs SVGProcessor.normalizeIds(svg, idPrefix); // Create the transform group xformGroup = new OMSVGGElement(); xformGroup.getTransform().getBaseVal().appendItem(svg.createSVGTransform()); // Build the geometry group (used to control the viewing // transform) OMSVGGElement elementGroup = new OMSVGGElement(); SVGNamedElementModel.createTitleDesc(elementGroup.getElement().<SVGElement>cast(), title); SVGProcessor.reparent(svg, elementGroup); // Create the selection group to handle highlighting // of the selection and hovered elements. OMSVGGElement twinGroup = (OMSVGGElement) elementGroup.cloneNode(true); twinGroup.getTransform().getBaseVal().appendItem(svg.createSVGTransform()); SVGProcessor.normalizeIds(twinGroup, SVGProcessor.newPrefixExtension(idPrefix, EXT_TWIN)); twinGroup.getStyle().setSVGProperty(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_HIDDEN_VALUE); modelGroup = create(elementGroup.getElement().<SVGElement>cast(), twinGroup.getElement().<SVGElement>cast()); xformGroup.appendChild(elementGroup); svg.appendChild(xformGroup); svg.appendChild(twinGroup); setScale(1f); // Build the SVG tree store store = new TreeStore<SVGElementModel>(); store.add(modelGroup, true); // From now on, listen to changes to the model, to translate // them into commands store.addStoreListener(new StoreListener<SVGElementModel>() { @Override public void storeUpdate(StoreEvent<SVGElementModel> se) { SVGModel.this.storeUpdate(se); } }); // Keep the viewBox if available for later computation of the viewBox model // on svg attach. The actual SVG viewBox is removed. OMSVGRect viewBoxRect = svg.getViewBox().getBaseVal(); if (viewBoxRect.getWidth() != 0f && viewBoxRect.getHeight() != 0f) { createViewBox(viewBoxRect); } svg.removeAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE); // Build the command store commandStore = new CommandStore(); // Build the selection model selectionModel = new SVGSelectionModel(); } /** * Return true if the specified SVG element is part of this model * @param element the element to test * @return true if the specified SVG element is part of this model */ public boolean contains(SVGElement element) { return elementToModel.containsKey(element); } protected void adopt(SVGElementModel model) { adopt(model, true); } protected void adopt(SVGElementModel model, boolean root) { model.setOwner(this); elementToModel.put(model.getElement(), model); elementToModel.put(model.getTwin(), model); // Do a DFS-preorder traversal of the DOM tree SVGElementModel firstChild = (SVGElementModel) model.getChild(0); if (firstChild != null) { adopt(firstChild, false); } if (!root) { SVGElementModel nextSibling = model.getNextSibling(); if (nextSibling != null) { adopt(nextSibling, false); } } } protected void orphan(SVGElementModel model) { orphan(model, true); } protected void orphan(SVGElementModel model, boolean root) { model.setOwner(null); elementToModel.remove(model.getElement()); elementToModel.remove(model.getTwin()); // Do a DFS-preorder traversal of the DOM tree SVGElementModel firstChild = (SVGElementModel) model.getChild(0); if (firstChild != null) { orphan(firstChild, false); } if (!root) { SVGElementModel nextSibling = model.getNextSibling(); if (nextSibling != null) { orphan(nextSibling, false); } } } public SVGElementModel create(Node modelNode, Node modelTwin) { Stack<Node> stack = new Stack<Node>(); stack.push(modelNode); stack.push(modelTwin); while(!stack.empty()) { Node twin = stack.pop(); Node node = stack.pop(); if (SVGProcessor.isSvgElement(node)) { if (SVGProcessor.isTitleDescElement(node.<SVGElement>cast())) { continue; } // if (SVGProcessor.isDefinitionElement(node.<SVGElement>cast())) { // continue; // } // if (SVGProcessor.isGraphicalElement(node.<SVGElement>cast())) { SVGElementModel model = convert(node.<SVGElement>cast()); if (model == null) { model = create(this, node, twin); adopt(model, false); } SVGElementModel parentModel = (SVGElementModel) model.getParent(); if (parentModel == null) { parentModel = convert(node.getParentElement().<SVGElement>cast()); } if (parentModel != null) { parentModel.add(model); } // } } NodeList<Node> nodeChildren = node.getChildNodes(); NodeList<Node> twinChildren = twin.getChildNodes(); for (int i = nodeChildren.getLength() - 1; i >= 0; i--) { stack.push(nodeChildren.getItem(i)); stack.push(twinChildren.getItem(i)); } } return convert(modelNode.<SVGElement>cast()); } private final native SVGElementModel create(SVGModel owner, Node element, Node twin) /*-{ var type = @org.vectomatic.dom.svg.utils.DOMHelper::getType(Lcom/google/gwt/core/client/JavaScriptObject;)(element); if (type) { var ctor = $wnd.otToModel[type]; if (ctor != null) { return ctor(owner, element, twin); } else { if (@org.vectomatic.svg.edit.client.engine.SVGProcessor::isTransformable(Lcom/google/gwt/dom/client/Node;)(element)) { return @org.vectomatic.svg.edit.client.model.svg.SVGGenericTransformableModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGElement;Lorg/vectomatic/dom/svg/impl/SVGElement;)(owner, element, twin); } return @org.vectomatic.svg.edit.client.model.svg.SVGGenericElementModel::new(Lorg/vectomatic/svg/edit/client/engine/SVGModel;Lorg/vectomatic/dom/svg/impl/SVGElement;Lorg/vectomatic/dom/svg/impl/SVGElement;)(owner, element, twin); } } return null; }-*/; /** * Appends a model to the children of the specified model * @param parentModel The parent model * @param model The model to append */ public void add(SVGElementModel parentModel, SVGElementModel model) { insertBefore(parentModel, model, null); } /** * Insert a new model into this SVG model before the specified child model * or the specified parent model. * @param parentModel The parent model * @param newModel The model to insert. If the model is not in this SVG model, * it is removed from its previous SVG model before being inserted into this SVG model. * @param refModel The reference model. If null, the new model is appended to * the children of the parent model */ public void insertBefore(SVGElementModel parentModel, SVGElementModel newModel, SVGElementModel refModel) { // Sanity checks Element element = newModel.getElement(); Element parentElement = parentModel.getElement(); assert(parentElement != null); assert(contains(parentElement.<SVGElement>cast())) : parentModel.toString() + " element is not in this model"; Element refElement = refModel != null ? refModel.getElement() : null; if (refElement != null) { assert(contains(refElement.<SVGElement>cast())) : refElement.toString() + " element is not in this model"; } Element twin = newModel.getTwin(); Element parentTwin = parentModel.getTwin(); assert(parentTwin != null); assert(contains(parentTwin.<SVGElement>cast())) : parentModel.toString() + " twin is not in this model"; Element refTwin = refModel != null ? refModel.getTwin() : null; if (refElement != null) { assert(contains(refTwin.<SVGElement>cast())) : refElement.toString() + " twin is not in this model"; } // Update SVG models SVGModel owner = newModel.getOwner(); if (owner != null) { owner.remove(newModel); } adopt(newModel); // Update the DOM tree parentElement.insertBefore(element, refElement); // Update the twin DOM tree parentTwin.insertBefore(twin, refTwin); if (refModel == null) { // Update the model tree parentModel.add(newModel); // Update the store store.add(parentModel, newModel, true); } else { int index = parentModel.indexOf(refModel); // Update the model tree parentModel.insert(newModel, index); // Update the store store.insert(parentModel, newModel, index, true); } } /** * Removes the specified model * @param model */ public void remove(SVGElementModel model) { if (model.getOwner() == this) { // Update the SVG model orphan(model); // Update the DOM tree Element element = model.getElement(); Element parentElement = element.getParentElement(); if (parentElement != null) { parentElement.removeChild(element); } Element twin = model.getTwin(); Element parentTwin = twin.getParentElement(); if (parentTwin != null) { parentTwin.removeChild(twin); } // Update the model tree if (model.getParent() != null) { model.getParent().remove(model); } // Update the store store.removeAll(model); store.remove(model); } } /** * Recursively clones the specified model. * @param model * @return the root of the cloned tree */ public SVGElementModel clone(SVGElementModel model, String name) { SVGElementModel clone = create(model.getElement().cloneNode(true).<SVGElement>cast(), model.getTwin().cloneNode(true).<SVGElement>cast()); clone.set(SVGConstants.SVG_TITLE_TAG, name); return clone; } protected void storeUpdate(StoreEvent<SVGElementModel> se) { ICommandFactory factory = SvgrealApp.getApp().getCommandFactorySelector().getActiveFactory(); if (factory instanceof StoreEventProcessor) { ((StoreEventProcessor)factory).processStoreEvent(se); } } public String getMarkup() { OMSVGSVGElement svg = new OMSVGSVGElement(); svg.setAttribute(SVGConstants.XMLNS_PREFIX + ":" + SVGConstants.XLINK_PREFIX, SVGConstants.XLINK_NAMESPACE_URI); svg.setViewBox( viewBox.<Float>get(SVGConstants.SVG_X_ATTRIBUTE), viewBox.<Float>get(SVGConstants.SVG_Y_ATTRIBUTE), viewBox.<Float>get(SVGConstants.SVG_WIDTH_ATTRIBUTE), viewBox.<Float>get(SVGConstants.SVG_HEIGHT_ATTRIBUTE)); SVGGElement g = modelGroup.getElement().cloneNode(true).cast(); // Skip the element representing the viewbox Node viewBoxElement = DOMHelper.evaluateNodeXPath(g, "//svg:rect[@" + ATTR_KIND + "='" + ATTR_KIND_VIEWBOX + "']", SVGPrefixResolver.INSTANCE); Node node = null; while((node = g.getFirstChild()) != null) { Node child = g.removeChild(node); if (child != viewBoxElement) { svg.getElement().appendChild(child); } } return svg.getMarkup(); } /** * Returns the root node of this model * @return */ public SVGElementModel getRoot() { return store.getRootItems().get(0); } /*========================================================== * * G E T T E R S * *==========================================================*/ /** * Returns the Store which contains this model data * @return */ public TreeStore<SVGElementModel> getStore() { return store; } /** * Returns the CommandStore which contains this model commands * @return */ public CommandStore getCommandStore() { return commandStore; } /** * Returns the selection model. * @return the selection model. */ public TreePanelSelectionModel<SVGElementModel> getSelectionModel() { return selectionModel; } /** * Returns the model viewBox. * @return the model viewBox. */ public SVGViewBoxElementModel getViewBox() { return viewBox; } /** * Returns the document id prefix. * @return the document id prefix. */ public String getIdPrefix() { return idPrefix; } /*========================================================== * * E V E N T H A N D L I N G * *==========================================================*/ @Override public void onMouseMove(MouseMoveEvent event) { // Forward to manipulator ICommandFactory commandFactory = SvgrealApp.getApp().getCommandFactorySelector().getActiveFactory(); if (commandFactory instanceof MouseMoveProcessor) { if (((MouseMoveProcessor)commandFactory).processMouseMove(event)) { return; } } // Highlighting if (highlightingMode) { SVGElementModel model = convert(event.getNativeEvent().getEventTarget().<SVGElement>cast()); highlightModel(model); } } @Override public void onMouseDown(MouseDownEvent event) { GWT.log("SVGModel.onMouseDown"); SVGElement target = event.getNativeEvent().getEventTarget().cast(); // Context menu if (event.getNativeButton() == 2) { SVGElementModel model = convert(target); if (model == null || SVGConstants.SVG_SVG_TAG.equals(target.getTagName())) { // Empty selection or unknown element selectionModel.deselectAll(); } else { if (!selectionModel.isSelected(model)) { // mono selection selectionModel.select(model, false); } /* else { // mono or multiselection } */ } return; } // Forward to manipulator ICommandFactory commandFactory = SvgrealApp.getApp().getCommandFactorySelector().getActiveFactory(); if (commandFactory instanceof MouseDownProcessor) { if (((MouseDownProcessor)commandFactory).processMouseDown(event)) { return; } } // Selection if (SVGConstants.SVG_SVG_TAG.equals(target.getTagName())) { selectionModel.deselectAll(); } else { SVGElementModel model = convert(target); if (model != null) { if (selectionModel.isSelected(model)) { if (event.isControlKeyDown()) { // Toggle selection selectionModel.deselect(model); } } else if (event.isShiftKeyDown() | event.isControlKeyDown()) { // Add to selection selectionModel.select(model, true); } else { // New selection selectionModel.select(model, false); } } } } @Override public void onMouseUp(MouseUpEvent event) { GWT.log("SVGModel.onMouseUp"); ICommandFactory commandFactory = SvgrealApp.getApp().getCommandFactorySelector().getActiveFactory(); if (commandFactory instanceof MouseUpProcessor) { ((MouseUpProcessor)commandFactory).processMouseUp(event); } } public void onKeyPress(ComponentEvent event) { int code = event.getKeyCode(); GWT.log("SVGModel.onKeyPress: " + code); ICommandFactory commandFactory = SvgrealApp.getApp().getCommandFactorySelector().getActiveFactory(); if (commandFactory instanceof KeyPressProcessor && ((KeyPressProcessor)commandFactory).processKeyPress(event)) { return; } if (code == KeyCodes.KEY_DELETE || code == KeyCodes.KEY_BACKSPACE) { RemoveElementsCommandFactory.INSTANTIATOR.create().start(this); } if (code == KeyNavExt.KEY_F2) { List<SVGElementModel> selectedItems = selectionModel.getSelectedItems(); if (selectedItems.size() == 1) { SVGElementModel model = selectedItems.get(0); SvgrealApp.getApp().getWindow(model.getElement()).renameModel(model); } } } public void onKeyUp(ComponentEvent event) { int code = event.getKeyCode(); GWT.log("SVGModel.onKeyUp: " + code); ICommandFactory commandFactory = SvgrealApp.getApp().getCommandFactorySelector().getActiveFactory(); if (commandFactory instanceof KeyUpProcessor && ((KeyUpProcessor)commandFactory).processKeyUp(event)) { return; } } /** * Updates the context menu based on the model selection * @param contextMenu The context menu to update */ public void updateContextMenu(Menu contextMenu) { List<SVGElementModel> selectedModels = selectionModel.getSelectedItems(); List<Item> items = new ArrayList<Item>(); int size = selectedModels.size(); if (size == 0) { // Empty selection items.add(new CommandFactoryMenuItem(AddLineCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddCircleCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddEllipseCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddRectCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddPolylineCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddPolygonCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddPathCommandFactory.INSTANTIATOR)); items.add(new CommandFactoryMenuItem(AddGroupCommandFactory.INSTANTIATOR)); } else if (size == 1) { // Mono selection MetaModel metaModel = selectedModels.get(0).getMetaModel(); items.addAll(metaModel.getContextMenuItems()); items.add(new CommandFactoryMenuItem(RemoveElementsCommandFactory.INSTANTIATOR)); } else { // Multi selection items.add(new CommandFactoryMenuItem(RemoveElementsCommandFactory.INSTANTIATOR)); } items.add(new CommandFactoryMenuItem(ShowPropertiesCommandFactory.INSTANTIATOR)); contextMenu.removeAll(); for (Item item : items) { contextMenu.add(item); } } /** * Returns the coordinates of a mouse event, converted * to the coordinate system of the model * @param e * A mouse event * @param snap * True if the coordinate should be snapped to the grid when * grid snapping is activated * @return * The coordinates of the mouse event, converted * to the coordinate system of the specified matrix */ public OMSVGPoint getCoordinates(MouseEvent<? extends EventHandler> e, boolean snap) { OMSVGMatrix m = modelGroup.getElement().<SVGGElement>cast().getScreenCTM().inverse(); OMSVGPoint p = svg.createSVGPoint(e.getClientX(), e.getClientY()).matrixTransform(m); return grid.snapsToGrid() ? grid.snap(p) : p; } /*========================================================== * * C A N V A S S I Z I N G * *==========================================================*/ public void onAttach() { // Create a viewbox for model which do not define one. // The viewbox is created to be 10% larger that the bbox if (viewBox == null) { GWT.log(svg.getBBox().getDescription()); createViewBox(svg.getBBox().inset(svg.createSVGRect(), -0.1f * svg.getBBox().getWidth(), -0.1f * svg.getBBox().getHeight())); } if (!grid.isAttached()) { grid.attach(this); svg.insertBefore(grid.getDefs(), xformGroup); xformGroup.getElement().insertBefore(grid.getRoot().getElement(), modelGroup.getElement()); } // Validate the model modelGroup.computeSeverity(); } private void createViewBox(OMSVGRect viewBoxRect) { OMSVGDocument document = (OMSVGDocument) svg.getOwnerDocument(); OMSVGRectElement rect = document.createSVGRectElement(viewBoxRect); rect.getStyle().setSVGProperty(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); rect.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); rect.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_DASHARRAY_PROPERTY, "4, 2"); rect.setAttribute(ATTR_KIND, ATTR_KIND_VIEWBOX); SVGRectElement element = rect.getElement().cast(); SVGRectElement twin = element.cloneNode(true).cast(); viewBox = new SVGViewBoxElementModel(this, element, twin); adopt(viewBox); insertBefore(modelGroup, viewBox, (SVGElementModel) modelGroup.getChild(0) /* will return null if geometryGroup has not children*/); } /** * Returns the scaling of the SVG. * @return The scale (1 means scale 1:1, 2 means scale 2:1) */ public float getScale() { return scale; } /** * Sets the scaling of the SVG. * @param scale * The scale (1 means scale 1:1, 2 means scale 2:1) */ public void setScale(float scale) { this.scale = scale; updateTransform(); fireEvent(new ScalingEvent(scale)); } public float getRotation() { return angle; } /** * Sets the rotation of the SVG. * @param angle * The angle (in degrees) */ public void setRotation(float angle) { this.angle = angle; updateTransform(); fireEvent(new RotationEvent(angle)); } /** * Returns this document grid * @return */ public Grid getGrid() { return grid; } @Override public void fireEvent(GwtEvent<?> event) { SvgrealApp.getApp().getEventBus().fireEventFromSource(event, this); } @Override public HandlerRegistration addRotationHandler(RotationHandler handler) { return SvgrealApp.getApp().getEventBus().addHandlerToSource(RotationEvent.getType(), this, handler); } @Override public HandlerRegistration addScalingHandler(ScalingHandler handler) { return SvgrealApp.getApp().getEventBus().addHandlerToSource(ScalingEvent.getType(), this, handler); } /** * Specifies the dimensions of the window viewport * @param width width of the window viewport * @param height height of the window viewport */ public void setWindowRect(int width, int height) { GWT.log("setWindowRect(" + width + ", " + height + ")"); windowRect.setWidth(width); windowRect.setHeight(height); updateTransform(); } public OMSVGRect getWindowRect() { return windowRect; } /** * Updates the display group transform and changes the CSS size * of the SVG accordingly */ public void updateTransform() { if (viewBox != null) { OMSVGRect bbox = ((SVGRectElement)viewBox.getElement()).getBBox(); // GWT.log("bbox = " + bbox.getDescription()); float d = (float)Math.sqrt((bbox.getWidth() * bbox.getWidth() + bbox.getHeight() * bbox.getHeight()) * 0.25) * scale * 2; // GWT.log("d = " + d); // Compute the actual canvas size. It is the max of the window rect // and the transformed bbox. float width = Math.max(d, windowRect.getWidth()); float height = Math.max(d, windowRect.getHeight()); // GWT.log("width = " + width); // GWT.log("height = " + height); // Compute the display transform to center the image in the // canvas OMSVGMatrix m = svg.createSVGMatrix(); float cx = bbox.getCenterX(); float cy = bbox.getCenterY(); m = m.translate(0.5f * (width - bbox.getWidth()) -bbox.getX(), 0.5f * (height - bbox.getHeight()) -bbox.getY()) .translate(cx, cy) .rotate(angle) .scale(scale) .translate(-cx, -cy); ((ISVGTransformable)xformGroup).getTransform().getBaseVal().getItem(0).setMatrix(m); ((ISVGTransformable)modelGroup.getTwinWrapper()).getTransform().getBaseVal().getItem(0).setMatrix(m); // GWT.log("m=" + m.getDescription()); svg.getStyle().setWidth(width, Unit.PX); svg.getStyle().setHeight(height, Unit.PX); } } /** * Computes the size of the vertex representation (it should be 1mm * whatever the scaling factor). * @return */ public static float getVertexSize(SVGElementModel model) { return getVertexSize((ISVGLocatable)model.getElementWrapper()); } public static float getVertexSize(ISVGLocatable element) { OMSVGMatrix m = element.getScreenCTM().inverse(); // 1mm = 3.543307px float a = 3.543307f * m.getA(); float b = 3.543307f * m.getD(); return (float)Math.sqrt(a * a + b * b); } /*========================================================== * * H I G H L I G H T I N G * *==========================================================*/ public OMSVGGElement getElementGroup() { return (OMSVGGElement) modelGroup.getElementWrapper(); } public OMSVGGElement getTwinGroup() { return (OMSVGGElement) modelGroup.getTwinWrapper(); } public boolean isHighlightingMode() { return highlightingMode; } public void setHighlightingMode(boolean highlightingMode) { if (highlightingMode != this.highlightingMode) { // GWT.log("setHighlightingMode(" + highlightingMode + ")"); this.highlightingMode = highlightingMode; float opacity = this.highlightingMode ? 0.25f : 1f; modelGroup.getElementWrapper().getStyle().setSVGProperty(SVGConstants.CSS_OPACITY_PROPERTY, Float.toString(opacity)); for (SVGElementModel model : selectionModel.getSelectedItems()) { displayTwin(model, highlightingMode); } highlightModel(null); } } public void highlightModel(SVGElementModel model) { if (model != highlightedModel) { // GWT.log("highlightModel(" + (model != null ? model.getTwin() : null) + ")"); if (highlightedModel != null && !selectionModel.isSelected(highlightedModel)) { displayTwin(highlightedModel, false); } if (model != null) { displayTwin(model, true); } highlightedModel = model; } } public void displayTwin(SVGElementModel model, boolean value) { // GWT.log("displayTwin(" + ((model == null) ? "null" : model.getTwin()) + " ==> " + value + ")"); if (model != modelGroup) { OMSVGStyle style = model.getTwin().getStyle().cast(); if (style != null) { if (value) { style.setSVGProperty(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_VISIBLE_VALUE); // style.setSVGProperty(SVGConstants.CSS_POINTER_EVENTS_PROPERTY, SVGConstants.CSS_NONE_VALUE); } else { style.clearSVGProperty(SVGConstants.CSS_VISIBILITY_PROPERTY); } } } } /*========================================================== * * E L E M E N T N A M I N G * *==========================================================*/ public String generateName(SVGElementModel model) { String name = model.getMetaModel().getName(); if (name == null) { name = DOMHelper.getLocalName(model.getElement()); } Integer count = tagNameToTagCount.get(name); if (count == null) { count = 0; } tagNameToTagCount.put(name, count + 1); return name + (count + 1); } /*========================================================== * * R E F E R E N C E S O L V I N G * *==========================================================*/ public OMSVGElement dereference(String idref) { if (idref.startsWith("#")) { idref = idref.substring(1); } else if (idref.startsWith("url(#")) { idref = idref.substring(5, idref.length() - 1); } return OMSVGParser.currentDocument().getElementById(idref); } }