// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2017 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client.editor.simple.components; import static com.google.appinventor.client.Ode.MESSAGES; import com.google.appinventor.client.editor.simple.SimpleComponentDatabase; import com.google.appinventor.client.ComponentsTranslation; import com.google.appinventor.client.Images; import com.google.appinventor.client.Ode; import com.google.appinventor.client.editor.ProjectEditor; import com.google.appinventor.client.editor.simple.SimpleEditor; import com.google.appinventor.client.editor.simple.components.utils.PropertiesUtil; import com.google.appinventor.client.editor.youngandroid.YaBlocksEditor; import com.google.appinventor.client.editor.youngandroid.YaFormEditor; import com.google.appinventor.client.explorer.SourceStructureExplorerItem; import com.google.appinventor.client.explorer.project.Project; import com.google.appinventor.client.output.OdeLog; import com.google.appinventor.client.widgets.ClonedWidget; import com.google.appinventor.client.widgets.LabeledTextBox; import com.google.appinventor.client.widgets.dnd.DragSource; import com.google.appinventor.client.widgets.dnd.DragSourceSupport; import com.google.appinventor.client.widgets.dnd.DropTarget; import com.google.appinventor.client.widgets.properties.EditableProperties; import com.google.appinventor.client.widgets.properties.EditableProperty; import com.google.appinventor.client.widgets.properties.PropertyChangeListener; import com.google.appinventor.client.widgets.properties.PropertyEditor; import com.google.appinventor.client.widgets.properties.TextPropertyEditor; import com.google.appinventor.client.youngandroid.TextValidators; import com.google.appinventor.shared.rpc.project.HasAssetsFolder; import com.google.appinventor.shared.rpc.project.ProjectNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidAssetsFolder; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode; import com.google.appinventor.shared.settings.SettingsConstants; import com.google.appinventor.shared.storage.StorageUtil; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.dom.client.HasAllTouchHandlers; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.TouchCancelHandler; import com.google.gwt.event.dom.client.TouchEndHandler; import com.google.gwt.event.dom.client.TouchMoveHandler; import com.google.gwt.event.dom.client.TouchStartHandler; import com.google.gwt.event.dom.client.TouchCancelEvent; import com.google.gwt.event.dom.client.TouchEndEvent; import com.google.gwt.event.dom.client.TouchMoveEvent; import com.google.gwt.event.dom.client.TouchStartEvent; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event.NativePreviewEvent; import com.google.gwt.user.client.Random; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.DialogBox; import com.google.gwt.user.client.ui.Focusable; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.MouseListener; import com.google.gwt.user.client.ui.MouseListenerCollection; import com.google.gwt.user.client.ui.SourcesMouseEvents; import com.google.gwt.user.client.ui.TreeItem; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.google.appinventor.shared.simple.ComponentDatabaseInterface.ComponentDefinition; import com.google.appinventor.shared.simple.ComponentDatabaseInterface.PropertyDefinition; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; /** * Abstract superclass for all components in the visual designer. * * <p>Since the actual component implementation are for a target platform * that is different from the platform used to implement the development * environment, we need to mock them. * * @author [email protected] (Liz Looney) */ public abstract class MockComponent extends Composite implements PropertyChangeListener, SourcesMouseEvents, DragSource, HasAllTouchHandlers { // Common property names (not all components support all properties). public static final String PROPERTY_NAME_NAME = "Name"; public static final String PROPERTY_NAME_UUID = "Uuid"; private static final int ICON_IMAGE_WIDTH = 16; private static final int ICON_IMAGE_HEIGHT = 16; public static final int BORDER_SIZE = 2 + 2; // see ode-SimpleMockComponent in Ya.css /** * This class defines the dialog box for renaming a component. */ private class RenameDialog extends DialogBox { // UI elements private final LabeledTextBox newNameTextBox; RenameDialog(String oldName) { super(false, true); setStylePrimaryName("ode-DialogBox"); setText(MESSAGES.renameTitle()); VerticalPanel contentPanel = new VerticalPanel(); LabeledTextBox oldNameTextBox = new LabeledTextBox(MESSAGES.oldNameLabel()); oldNameTextBox.setText(getName()); oldNameTextBox.setEnabled(false); contentPanel.add(oldNameTextBox); newNameTextBox = new LabeledTextBox(MESSAGES.newNameLabel()); newNameTextBox.setText(oldName); newNameTextBox.getTextBox().addKeyUpHandler(new KeyUpHandler() { @Override public void onKeyUp(KeyUpEvent event) { int keyCode = event.getNativeKeyCode(); if (keyCode == KeyCodes.KEY_ENTER) { handleOkClick(); } else if (keyCode == KeyCodes.KEY_ESCAPE) { hide(); } } }); contentPanel.add(newNameTextBox); Button cancelButton = new Button(MESSAGES.cancelButton()); cancelButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { hide(); } }); Button okButton = new Button(MESSAGES.okButton()); okButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { handleOkClick(); } }); HorizontalPanel buttonPanel = new HorizontalPanel(); buttonPanel.add(cancelButton); buttonPanel.add(okButton); buttonPanel.setSize("100%", "24px"); contentPanel.add(buttonPanel); contentPanel.setSize("320px", "100%"); add(contentPanel); } private void handleOkClick() { String newName = newNameTextBox.getText(); // Remove leading and trailing whitespace // Replace nonempty sequences of internal spaces by underscores newName = newName.trim().replaceAll("[\\s\\xa0]+", "_"); if (newName.equals(getName())) { hide(); } else if (validate(newName)) { hide(); String oldName = getName(); changeProperty(PROPERTY_NAME_NAME, newName); getForm().fireComponentRenamed(MockComponent.this, oldName); } else { newNameTextBox.setFocus(true); newNameTextBox.selectAll(); } } private boolean validate(String newName) { // Check that it meets the formatting requirements. if (!TextValidators.isValidComponentIdentifier(newName)) { Window.alert(MESSAGES.malformedComponentNameError()); return false; } // Check that it's unique. final List<String> names = editor.getComponentNames(); if (names.contains(newName)) { Window.alert(MESSAGES.duplicateComponentNameError()); return false; } // Check that it is a variable name used in the Yail code if (TextValidators.isReservedName(newName)) { Window.alert(MESSAGES.reservedNameError()); return false; } //Check that it is not a Component type name, as this is bad for generics SimpleComponentDatabase COMPONENT_DATABASE = SimpleComponentDatabase.getInstance(); if (COMPONENT_DATABASE.isComponent(newName)) { Window.alert(MESSAGES.sameAsComponentTypeNameError()); return false; } return true; } @Override public void show() { super.show(); DeferredCommand.addCommand(new Command() { @Override public void execute() { newNameTextBox.setFocus(true); newNameTextBox.selectAll(); } }); } } /** * This class defines the dialog box for deleting a component. */ private class DeleteDialog extends DialogBox { DeleteDialog() { super(false, true); setStylePrimaryName("ode-DialogBox"); setText(MESSAGES.deleteComponentButton()); VerticalPanel contentPanel = new VerticalPanel(); contentPanel.add(new HTML(MESSAGES.reallyDeleteComponent())); Button cancelButton = new Button(MESSAGES.cancelButton()); cancelButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { hide(); } }); Button deleteButton = new Button(MESSAGES.deleteButton()); deleteButton.addStyleName("destructive-action"); deleteButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { hide(); MockComponent.this.delete(); } }); HorizontalPanel buttonPanel = new HorizontalPanel(); buttonPanel.add(cancelButton); buttonPanel.add(deleteButton); buttonPanel.setSize("100%", "24px"); contentPanel.add(buttonPanel); contentPanel.setSize("320px", "100%"); add(contentPanel); } @Override protected void onPreviewNativeEvent(NativePreviewEvent event) { super.onPreviewNativeEvent(event); switch (event.getTypeInt()) { case Event.ONKEYDOWN: if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) { hide(); } else if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) { hide(); MockComponent.this.delete(); } break; } } } // Component database: information about components (including their properties and events) private final SimpleComponentDatabase COMPONENT_DATABASE; // Image bundle protected static final Images images = Ode.getImageBundle(); // Empty component children array so that we don't have to special case and test for null in // case of no children private static final List<MockComponent> NO_CHILDREN = Collections.emptyList(); // Editor of Simple form source file the component belongs to protected final SimpleEditor editor; private final String type; private ComponentDefinition componentDefinition; private Image iconImage; private final SourceStructureExplorerItem sourceStructureExplorerItem; /** * The state of the branch in the components tree corresponding to this component. */ protected boolean expanded; // Properties of the component // Expose these to individual component subclasses, which might need to // check properties fpr UI manipulation. One example is MockHorizontalArrangement protected final EditableProperties properties; private DragSourceSupport dragSourceSupport; // Component container the component belongs to (this will be null for the root component aka the // form) private MockContainer container; private MouseListenerCollection mouseListeners = new MouseListenerCollection(); private HandlerManager handlers; /** * Creates a new instance of the component. * * @param editor editor of source file the component belongs to */ MockComponent(SimpleEditor editor, String type, Image iconImage) { this.editor = editor; this.type = type; this.iconImage = iconImage; this.handlers = new HandlerManager(this); COMPONENT_DATABASE = SimpleComponentDatabase.getInstance(editor.getProjectId()); componentDefinition = COMPONENT_DATABASE.getComponentDefinition(type); sourceStructureExplorerItem = new SourceStructureExplorerItem() { @Override public void onSelected() { // are we showing the blocks editor? if so, toggle the component drawer if (Ode.getInstance().getCurrentFileEditor() instanceof YaBlocksEditor) { YaBlocksEditor blocksEditor = (YaBlocksEditor) Ode.getInstance().getCurrentFileEditor(); OdeLog.log("Showing item " + getName()); blocksEditor.showComponentBlocks(getName()); } else { select(); } } @Override public void onStateChange(boolean open) { // The user has expanded or collapsed the branch in the components tree corresponding to // this component. Remember that by setting the expanded field so that when we re-build // the tree, we will keep the branch in the same state. expanded = open; } @Override public boolean canRename() { return !isForm(); } @Override public void rename() { if (!isForm()) { new RenameDialog(getName()).center(); } } @Override public boolean canDelete() { return !isForm(); } @Override public void delete() { if (!isForm()) { new DeleteDialog().center(); } } }; expanded = true; // Create a default property set for the component properties = new EditableProperties(true); // Add the mock component itself as a property change listener so that it can update its // visual aspects according to changes of its properties properties.addPropertyChangeListener(this); // Allow dragging this component in a drag-and-drop action if this is not the root form if (!isForm()) { dragSourceSupport = new DragSourceSupport(this); addMouseListener(dragSourceSupport); addTouchStartHandler(dragSourceSupport); addTouchMoveHandler(dragSourceSupport); addTouchEndHandler(dragSourceSupport); addTouchCancelHandler(dragSourceSupport); } } /** * Sets the components widget representation and initializes its properties. * * <p>To be called from implementing constructor. * * @param widget components visual representation in designer */ void initComponent(Widget widget) { // Widget needs to be initialized before the component itself so that the component properties // can be reflected by the widget initWidget(widget); // Capture mouse and click events in onBrowserEvent(Event) sinkEvents(Event.MOUSEEVENTS | Event.ONCLICK | Event.TOUCHEVENTS); // Add the special name property and set the tooltip String name = componentName(); setTitle(name); addProperty(PROPERTY_NAME_NAME, name, null, new TextPropertyEditor()); // TODO(user): Ensure this value is unique within the project using a list of // already used UUIDs // Set the component's UUID // The default value here can be anything except 0, because YoungAndroidProjectServce // creates forms with an initial Uuid of 0, and Properties.java doesn't encode // default values when it generates JSON for a component. addProperty(PROPERTY_NAME_UUID, "-1", null, new TextPropertyEditor()); changeProperty(PROPERTY_NAME_UUID, "" + Random.nextInt()); editor.getComponentPalettePanel().configureComponent(this); } public boolean isPropertyPersisted(String propertyName) { if (propertyName.equals(PROPERTY_NAME_NAME)) { return false; } return true; } protected boolean isPropertyVisible(String propertyName) { if (propertyName.equals(PROPERTY_NAME_NAME) || propertyName.equals(PROPERTY_NAME_UUID)) { return false; } return true; } protected boolean isPropertyforYail(String propertyName) { // By default we use the same criterion as persistance // This method can then be overriden by the invididual // component Mocks return isPropertyPersisted(propertyName); } /** * Invoked after a component is created from the palette. * * <p>Some subclasses may wish to override this method to initialize * properties of the newly created component. For example, a component with a * caption may want to initialize the caption to match the component's name. */ public void onCreateFromPalette() { } /** * Returns a unique default component name. */ private String componentName() { String compType = ComponentsTranslation.getComponentName(getType()); compType = compType.replace(" ", "_").replace("'", "_"); // Make sure it doesn't have any spaces in it return compType + getNextComponentIndex(); } /** * All components have default names for new component instantiations, * usually consisting of the type name and an index. This method * returns the next available component index for this component's type. * * We lower case the typeName and cName so we don't wind up with * components of the names 'fooComponent1' and 'FooComponent1' where * the only difference is the case of the first (or other) * letters. Ultimately the case does matter but when gensyming new * component names components whose only difference is in case will * still result in an incremented index. So if 'fooComponent1' exist * the new component will be 'FooComponent2' instead of * 'FooComponent1'. Hopefully this will be less confusing. * */ private int getNextComponentIndex() { int highIndex = 0; if (editor != null) { final String typeName = ComponentsTranslation.getComponentName(getType()) .toLowerCase() .replace(" ", "_") .replace("'", "_"); final int nameLength = typeName.length(); for (String cName : editor.getComponentNames()) { cName = cName.toLowerCase(); try { if (cName.startsWith(typeName)) { highIndex = Math.max(highIndex, Integer.parseInt(cName.substring(nameLength))); } } catch (NumberFormatException e) { continue; } } } return highIndex + 1; } /** * Adds a new property for the component. * * @param name property name * @param defaultValue default value of property * @param caption property's caption for use in the ui * @param editor property editor */ public final void addProperty(String name, String defaultValue, String caption, PropertyEditor editor) { int type = EditableProperty.TYPE_NORMAL; if (!isPropertyPersisted(name)) { type |= EditableProperty.TYPE_NONPERSISTED; } if (!isPropertyVisible(name)) { type |= EditableProperty.TYPE_INVISIBLE; } if (isPropertyforYail(name)) { type |= EditableProperty.TYPE_DOYAIL; } properties.addProperty(name, defaultValue, caption, editor, type); } /** * Returns the component name. * <p> * This should not be called prior to {@link #initComponent(Widget)}. * * @return component name */ public String getName() { return properties.getPropertyValue(PROPERTY_NAME_NAME); } /** * Returns true if there is a property with the given name. * * @param name property name * @return true if the property exists */ public boolean hasProperty(String name) { return properties.getProperty(name) != null; } /** * Returns the property's value. * * @param name property name * @return property value */ public String getPropertyValue(String name) { return properties.getPropertyValue(name); } /** * Changes the value of a component property. * * @param name property name * @param value new property value */ public void changeProperty(String name, String value) { properties.changePropertyValue(name, value); } /** * Returns the properties set for the component. * * @return properties */ public EditableProperties getProperties() { return properties; } /** * Returns the children of this component. Note that the return value will * never be {@code null} but rather an empty array for components without * children. * <p> * The returned list should not be modified. * * @return children of the component */ public List<MockComponent> getChildren() { return NO_CHILDREN; } /** * Returns the visible children of this component that should be showing. * <p> * The returned list should not be modified. */ public final List<MockComponent> getShowingVisibleChildren() { List<MockComponent> allChildren = getChildren(); if (allChildren.size() == 0) { return NO_CHILDREN; } List<MockComponent> showingVisibleChildren = new ArrayList<MockComponent>(); for (MockComponent child : allChildren) { if (child.isVisibleComponent() && child.showComponentInDesigner()) { showingVisibleChildren.add(child); } } return showingVisibleChildren; } /** * Returns the visible children of this component that should be hidden. * <p> * The returned list should not be modified. */ public final List<MockComponent> getHiddenVisibleChildren() { List<MockComponent> allChildren = getChildren(); if (allChildren.size() == 0) { return NO_CHILDREN; } List<MockComponent> hiddenVisibleChildren = new ArrayList<MockComponent>(); for (MockComponent child : allChildren) { if (child.isVisibleComponent() && !child.showComponentInDesigner()) { hiddenVisibleChildren.add(child); } } return hiddenVisibleChildren; } /** * Returns the form containing this component. * * @return containing form */ public MockForm getForm() { return getContainer().getForm(); } public boolean isForm() { return false; } /** * Indicates whether a component has a visible representation. * <p> * The return value of this method will not change upon successive invocations. * * @return {@code true} if there is a visible representation for the * component, otherwise {@code false} */ public abstract boolean isVisibleComponent(); /** * Selects this component in the visual editor. */ public final void select() { getForm().setSelectedComponent(this); } /** * Invoked when the selection state of this component changes. * <p> * Implementations may override this method to perform additional * alterations to their appearance based on their new selection state. * Overriders must call {@code super.onSelectedChange(selected)} * before performing their own alterations. */ protected void onSelectedChange(boolean selected) { if (selected) { addStyleDependentName("selected"); } else { removeStyleDependentName("selected"); } getForm().fireComponentSelectionChange(this, selected); } /** * Returns whether this component is selected. */ public boolean isSelected() { return (getForm().getSelectedComponent() == this); } /** * Returns the type of the component. * The return value must not change between invocations. * <p> * This is used in the serialization format of the component. * * @return component type */ public final String getType() { return type; } /** * Returns the user-visible type name of the component. * By default this is the internal type string. * * @return component type name */ public String getVisibleTypeName() { return getType(); } /** * Returns the icon's image for the component (e.g. to be used on the component palette). * The return value must not change between invocations. * * @return icon for the component */ public final Image getIconImage() { return iconImage; } /** * Returns the unique id for the component * * @return uuid for the component */ public final String getUuid() { return getPropertyValue(PROPERTY_NAME_UUID); } /** * Sets the component container to which the component belongs. * * @param container owning component container for this component */ protected final void setContainer(MockContainer container) { this.container = container; } /** * Returns the component container to which the component belongs. * * @return owning component container for this component */ protected final MockContainer getContainer() { return container; } private final Focusable nullFocusable = new Focusable() { @Override public int getTabIndex() { return 0; } @Override public void setAccessKey(char key) { } @Override public void setFocus(boolean focused) { } @Override public void setTabIndex(int index) { } }; /** * Constructs a tree item for the component which will be displayed in the * source structure explorer. * * @return tree item for this component */ protected TreeItem buildTree() { // Instantiate new tree item for this component // Note: We create a ClippedImagePrototype because we need something that can be // used to get HTML for the iconImage. AbstractImagePrototype requires // an ImageResource, which we don't necessarily have. TreeItem itemNode = new TreeItem( new HTML("<span>" + iconImage.getElement().getString() + SafeHtmlUtils.htmlEscapeAllowEntities(getName()) + "</span>")) { @Override protected Focusable getFocusable() { return nullFocusable; } }; itemNode.setUserObject(sourceStructureExplorerItem); return itemNode; } /** * If this component isn't a Form, and this component's type isn't already in typesAndIcons, * adds this component's type name as a key to typesAndIcons, mapped to the HTML string used * to display the component type's icon. Subclasses that contain components should override * this to add their own info as well as that for their contained components. * @param typesAndIcons */ public void collectTypesAndIcons(Map<String, String> typesAndIcons) { String name = getVisibleTypeName(); if (!isForm() && !typesAndIcons.containsKey(name)) { typesAndIcons.put(name, iconImage.getElement().getString()); } } /** * Returns the source structure explorer item for this component. */ public final SourceStructureExplorerItem getSourceStructureExplorerItem() { return sourceStructureExplorerItem; } /** * Returns the asset node with the given name. * * @param name asset name * @return asset node found or {@code null} */ protected ProjectNode getAssetNode(String name) { Project project = Ode.getInstance().getProjectManager().getProject(editor.getProjectId()); if (project != null) { HasAssetsFolder<YoungAndroidAssetsFolder> hasAssetsFolder = (YoungAndroidProjectNode) project.getRootNode(); for (ProjectNode asset : hasAssetsFolder.getAssetsFolder().getChildren()) { if (asset.getName().equals(name)) { return asset; } } } return null; } /** * Converts the given image property value to an image url. * Returns null if the image property value is blank or not recognized as an * asset. */ protected String convertImagePropertyValueToUrl(String text) { if (text.length() > 0) { ProjectNode asset = getAssetNode(text); if (asset != null) { return StorageUtil.getFileUrl(asset.getProjectId(), asset.getFileId()); } } return null; } // For debugging purposes only private String describeElement(com.google.gwt.dom.client.Element element) { if (element == null) { return "null"; } if (element == getElement()) { return "this"; } try { return element.getTagName(); } catch (com.google.gwt.core.client.JavaScriptException e) { // Can get here if the browser throws a permission denied error return "????"; } } /** * Invoked by GWT whenever a browser event is dispatched to this component. */ @Override public void onBrowserEvent(Event event) { if (!shouldCancel(event)) return; switch (event.getTypeInt()) { case Event.ONTOUCHSTART: case Event.ONTOUCHEND: if (isForm()) { select(); } case Event.ONTOUCHMOVE: case Event.ONTOUCHCANCEL: cancelBrowserEvent(event); DomEvent.fireNativeEvent(event, handlers); break; case Event.ONMOUSEDOWN: case Event.ONMOUSEUP: case Event.ONMOUSEMOVE: case Event.ONMOUSEOVER: case Event.ONMOUSEOUT: cancelBrowserEvent(event); mouseListeners.fireMouseEvent(this, event); break; case Event.ONCLICK: cancelBrowserEvent(event); select(); break; default: // Ignore unexpected events break; } } /* * Prevent browser from doing its own event handling and consume event */ private static void cancelBrowserEvent(Event event) { DOM.eventPreventDefault(event); DOM.eventCancelBubble(event, true); } // SourcesMouseEvents implementation /** * Adds the specified mouse-listener to this component's widget. * The listener will be notified of mouse events. */ @Override public final void addMouseListener(MouseListener listener) { mouseListeners.add(listener); } /** * Removes the specified mouse-listener from this component's widget. */ @Override public final void removeMouseListener(MouseListener listener) { mouseListeners.remove(listener); } @Override public final HandlerRegistration addTouchStartHandler(TouchStartHandler handler) { return handlers.addHandler(TouchStartEvent.getType(), handler); } @Override public final HandlerRegistration addTouchMoveHandler(TouchMoveHandler handler) { return handlers.addHandler(TouchMoveEvent.getType(), handler); } @Override public final HandlerRegistration addTouchEndHandler(TouchEndHandler handler) { return handlers.addHandler(TouchEndEvent.getType(), handler); } @Override public final HandlerRegistration addTouchCancelHandler(TouchCancelHandler handler) { return handlers.addHandler(TouchCancelEvent.getType(), handler); } // DragSource implementation @Override public final void onDragStart() { // no action until createDragWidget() is called } @Override public final Widget createDragWidget(int x, int y) { // TODO(user): Make sure the cloned widget does NOT appear in the // selected state, even if the original widget is in // the selected state. Widget w = new ClonedWidget(this); DragSourceSupport.configureDragWidgetToAppearWithCursorAt(w, x, y); // Hide this element, but keep taking up space in the UI. // This must be done after the drag-widget is created so that // the drag widget itself isn't hidden. setVisible(false); return w; } @Override public Widget getDragWidget() { return dragSourceSupport.getDragWidget(); } @Override public DropTarget[] getDropTargets() { final List<DropTarget> targetsWithinForm = getForm().getDropTargetsWithin(); return targetsWithinForm.toArray(new DropTarget[targetsWithinForm.size()]); } @Override public final void onDragEnd() { // Reshow this element setVisible(true); } /** * Returns the preferred width of the component if there was no layout restriction, * including the CSS border. * <p> * Callers should be aware that most components cannot calculate their * preferred size correctly until they are attached to the UI; see {@link #isAttached()}. * Unattached components are liable to return {@code 0} for any query about their preferred size. * * @return preferred width */ // TODO(user): see getPreferredHeight()! public int getPreferredWidth() { return MockComponentsUtil.getPreferredWidth(this); } /** * Returns the preferred height of the component if there was no layout restriction, * including the CSS border. * <p> * Callers should be aware that most components cannot calculate their * preferred size correctly until they are attached to the UI; see {@link #isAttached()}. * Unattached components are liable to return {@code 0} for any query about their preferred size. * * @return preferred height */ // TODO(user): The concept of preferred height/width is implemented completely wrong. // Currently we are taking the default size of GWT components. This should be // implemented to match the behavior of the Android components being mocked. public int getPreferredHeight() { return MockComponentsUtil.getPreferredHeight(this); } /* * Returns true if this component should be shown in the designer. */ private boolean showComponentInDesigner() { if (hasProperty(MockVisibleComponent.PROPERTY_NAME_VISIBLE)) { boolean visible = Boolean.parseBoolean(getPropertyValue( MockVisibleComponent.PROPERTY_NAME_VISIBLE)); // If this component's visible property is false, we need to check whether to show hidden // components. if (!visible) { YaFormEditor formEditor = (YaFormEditor) editor; return formEditor.shouldDisplayHiddenComponents(); } } return true; } int getWidthHint() { return Integer.parseInt(getPropertyValue(MockVisibleComponent.PROPERTY_NAME_WIDTH)); } int getHeightHint() { return Integer.parseInt(getPropertyValue(MockVisibleComponent.PROPERTY_NAME_HEIGHT)); } /** * Refreshes the form. * * <p>This method should be called whenever a property that affects the size * of the component is changed. It calls refreshForm(false) which permits * throttling. */ final void refreshForm() { refreshForm(false); } /* * Refresh the current form. If force is true, we bypass the * throttling code. This is needed by MockImageBase because it * *must* refresh the form before resizing loaded images. * */ final void refreshForm(boolean force) { if (isAttached()) { if (getContainer() != null || isForm()) { if (force) { getForm().doRefresh(); } else { getForm().refresh(); } } } } // PropertyChangeListener implementation @Override public void onPropertyChange(String propertyName, String newValue) { if (propertyName.equals(PROPERTY_NAME_NAME)) { setTitle(newValue); } else if (getContainer() != null || isForm()) { /* If we've already placed the component onto a Form (and therefore * into a container) then call fireComponentPropertyChanged(). * It's not really an instantiated component until its been added to * a container. If we don't make this test then we end up calling * fireComponentPropertyChanged when we start dragging the component from * the palette. We need to explicitly trigger on Form here, because forms * are not in containers. */ getForm().fireComponentPropertyChanged(this, propertyName, newValue); } } public void onRemoved() { } public void delete() { this.editor.getProjectEditor().clearLocation(getName()); getForm().select(); // Pass true to indicate that the component is being permanently deleted. getContainer().removeComponent(this, true); // tell the component its been removed, so it can remove children's blocks onRemoved(); properties.removePropertyChangeListener(this); properties.clear(); } // Layout LayoutInfo createLayoutInfo(Map<MockComponent, LayoutInfo> layoutInfoMap) { return new LayoutInfo(layoutInfoMap, this) { @Override int calculateAutomaticWidth() { return getPreferredWidth(); } @Override int calculateAutomaticHeight() { return getPreferredHeight(); } }; } /** Upgrading MockComponent * * When extensions are upgraded, the MockComponents might need to undergo changes. * These changes can be produced inside this function. * All subclasses overriding this method must call super.upgrade()! */ public void upgrade() { //Upgrade Icon //We copy all compatible properties values List<PropertyDefinition> newProperties = COMPONENT_DATABASE.getPropertyDefinitions(this.type); List<PropertyDefinition> oldProperties = componentDefinition.getProperties(); EditableProperties currentProperties = getProperties(); //Operations List<String> toBeRemoved = new ArrayList<String>(); List<String> toBeAdded = new ArrayList<String>(); //Plan operations for (EditableProperty property : currentProperties) { boolean presentInNewProperties = false; boolean presentInOldProperties = false; String oldType = ""; String newType = ""; for (PropertyDefinition prop : newProperties) { if (prop.getName().equals(property.getName())) { presentInNewProperties = true; newType = prop.getEditorType(); } } for (PropertyDefinition prop : oldProperties) { if (prop.getName().equals(property.getName())) { presentInOldProperties = true; oldType = prop.getEditorType(); } } // deprecated property if (!presentInNewProperties && presentInOldProperties) { toBeRemoved.add(property.getName()); } // new property, less likely to happen here else if (presentInNewProperties && !presentInOldProperties) { toBeAdded.add(property.getName()); } // existing property else if (presentInNewProperties && presentInOldProperties) { if (newType != oldType) { // type change detected toBeRemoved.add(property.getName()); toBeAdded.add(property.getName()); } } } //New property for (PropertyDefinition property : newProperties) { if (!toBeAdded.contains(property.getName()) && !currentProperties.hasProperty(property.getName())) { toBeAdded.add(property.getName()); } } //Execute operations for (String prop : toBeRemoved) { currentProperties.removeProperty(prop); } for (PropertyDefinition property : newProperties) { if (toBeAdded.contains(property.getName())) { PropertyEditor propertyEditor = PropertiesUtil.createPropertyEditor(property.getEditorType(), property.getDefaultValue(), (YaFormEditor) editor, property.getEditorArgs()); addProperty(property.getName(), property.getDefaultValue(), property.getCaption(), propertyEditor); } } } /** * upgradeComplete() * Mark a MockComponent upgrade complete. * This MUST be called manually after calling upgrade()! * All subclasses overriding this method must call super.upgradeComplete()! */ public void upgradeComplete() { this.componentDefinition = COMPONENT_DATABASE.getComponentDefinition(this.type); //Update ComponentDefinition } public native void setShouldCancel(Event event, boolean cancelable)/*-{ event.shouldNotCancel = !cancelable; }-*/; public native boolean shouldCancel(Event event)/*-{ return !event.shouldNotCancel; }-*/; }