com.google.appinventor.client.editor.simple.components.MockForm.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.client.editor.simple.components.MockForm.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 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 java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

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.properties.YoungAndroidLengthPropertyEditor;
import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidVerticalAlignmentChoicePropertyEditor;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.properties.BadPropertyEditorException;
import com.google.appinventor.client.widgets.properties.EditableProperties;
import com.google.appinventor.shared.settings.SettingsConstants;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.TreeItem;

/**
 * Mock Form component. This implementation provides two main preview sizes corresponding to
 * 'normal' and 'large' buckets (http://developer.android.com/guide/practices/screens_support.html).
 * Normal size is a 1:1 with pixels on a device with dpi:160. We use that as the baseline for the
 * browser too. All UI elements should be scaled to DP for buckets other than 'normal'.
 */
public final class MockForm extends MockContainer {

    /*
     * Widget for the mock form title bar.
     */
    private class TitleBar extends Composite {
        private static final int HEIGHT = 24;

        // UI elements
        private Label title;
        private AbsolutePanel bar;

        /*
         * Creates a new title bar.
         */
        TitleBar() {
            title = new Label();
            title.setStylePrimaryName("ode-SimpleMockFormTitle");
            title.setHorizontalAlignment(Label.ALIGN_LEFT);

            bar = new AbsolutePanel();
            bar.add(title, 12, 4);

            initWidget(bar);

            setStylePrimaryName("ode-SimpleMockFormTitleBar");
            setSize("100%", HEIGHT + "px");
        }

        /*
         * Changes the title in the title bar.
         */
        void changeTitle(String newTitle) {
            title.setText(newTitle);
        }
    }

    /*
     * Widget for a mock phone status bar.
     */
    private class PhoneBar extends Composite {
        private static final int HEIGHT = 24;

        // UI elements
        private DockPanel bar;
        private Image phoneBarImage;

        /*
         * Creates a new phone status bar.
         */
        PhoneBar() {
            phoneBarImage = new Image(images.phonebar());

            bar = new DockPanel();
            bar.setHorizontalAlignment(HorizontalPanel.ALIGN_RIGHT);
            bar.add(phoneBarImage, DockPanel.EAST);

            initWidget(bar);

            setStylePrimaryName("ode-SimpleMockFormPhoneBar");
            setSize("100%", HEIGHT + "px");
        }
    }

    /*
     *
     * Widget for a mock phone navigation bar; Shows at the bottom of the viewer
     */
    private class NavigationBar extends Composite {
        private static final int HEIGHT = 35;

        // UI elements
        private DockPanel bar;
        private Image navigationBarImage;

        /*
         * Creates a new phone navigation bar; Shows at the bottom of the viewer.
         */

        NavigationBar() {
            navigationBarImage = new Image(images.navigationbar());
            bar = new DockPanel();
            bar.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
            bar.add(navigationBarImage, DockPanel.CENTER);
            initWidget(bar);

            setStylePrimaryName("ode-SimpleMockFormPhoneBar"); //reuse the css for the phone
            setSize("100%", HEIGHT + "px");
        }
    }

    /**
     * Component type name.
     */
    public static final String TYPE = "Form";

    private static final String VISIBLE_TYPE = "Screen";

    // Currently App Inventor provides two main sizes that correspond to 'normal' and 'large'
    // screens. We use phone=normal (470 x 320 DP) and tablet=large (640 x 480 DP).
    // More information about 'bucket' sizes at:
    // http://developer.android.com/guide/practices/screens_support.html
    // The values for Phone and Tablet were decided by trial and error. The main reason is that in
    // the designer we use sizes of GWT widgets, and not the sizes of the actual Android widgets.

    private static final int PHONE_PORTRAIT_WIDTH = 320;
    private static final int PHONE_PORTRAIT_HEIGHT = 470 + 35; // Adds 35 for the navigation bar
    private static final int PHONE_LANDSCAPE_WIDTH = PHONE_PORTRAIT_HEIGHT;
    private static final int PHONE_LANDSCAPE_HEIGHT = PHONE_PORTRAIT_WIDTH;

    private static final int TABLET_PORTRAIT_WIDTH = 480;
    private static final int TABLET_PORTRAIT_HEIGHT = 640 + 35; // Adds 35 for the navigation bar
    private static final int TABLET_LANDSCAPE_WIDTH = TABLET_PORTRAIT_HEIGHT;
    private static final int TABLET_LANDSCAPE_HEIGHT = TABLET_PORTRAIT_WIDTH;

    // These are default values but they can be changed in the changePreviewSize method
    private int PORTRAIT_WIDTH = PHONE_PORTRAIT_WIDTH;
    private int PORTRAIT_HEIGHT = PHONE_PORTRAIT_HEIGHT;
    private int LANDSCAPE_WIDTH = PHONE_LANDSCAPE_WIDTH;
    private int LANDSCAPE_HEIGHT = PHONE_LANDSCAPE_HEIGHT;
    private boolean landscape = false;

    // Property names
    private static final String PROPERTY_NAME_TITLE = "Title";
    private static final String PROPERTY_NAME_SCREEN_ORIENTATION = "ScreenOrientation";
    private static final String PROPERTY_NAME_SCROLLABLE = "Scrollable";
    private static final String PROPERTY_NAME_ICON = "Icon";
    private static final String PROPERTY_NAME_VCODE = "VersionCode";
    private static final String PROPERTY_NAME_VNAME = "VersionName";
    private static final String PROPERTY_NAME_ANAME = "AppName";
    private static final String PROPERTY_NAME_SIZING = "Sizing"; // Don't show except on screen1

    // Form UI components
    AbsolutePanel formWidget;
    ScrollPanel scrollPanel;
    private TitleBar titleBar;
    private MockComponent selectedComponent;

    int screenWidth; // TEMP: Make package visible so we can use it MockHVLayoutBase
    private int screenHeight;
    int usableScreenHeight; // TEMP: Make package visible so we can use it MockHVLayoutBase

    // Set of listeners for any changes of the form
    final HashSet<FormChangeListener> formChangeListeners = new HashSet<FormChangeListener>();

    // Don't access the verticalScrollbarWidth field directly. Use getVerticalScrollbarWidth().
    private static int verticalScrollbarWidth;

    private MockFormLayout myLayout;

    // flag to control attempting to enable/disable vertical
    // alignment when scrollable property is changed
    private boolean initialized = false;

    private YoungAndroidVerticalAlignmentChoicePropertyEditor myVAlignmentPropertyEditor;

    public static final String PROPERTY_NAME_HORIZONTAL_ALIGNMENT = "AlignHorizontal";
    public static final String PROPERTY_NAME_VERTICAL_ALIGNMENT = "AlignVertical";

    /**
     * Creates a new MockForm component.
     *
     * @param editor  editor of source file the component belongs to
     */
    public MockForm(SimpleEditor editor) {
        // Note(Hal): This helper thing is a kludge because I really want to write:
        // myLayout = new MockHVLayout(orientation);
        // super(editor, type, icon, myLayout);
        // but Java won't let me do that.

        super(editor, TYPE, images.form(), MockFormHelper.makeLayout());
        // Note(hal): There better not be any calls to MockFormHelper before the
        // next instruction.  Note that the Helper methods are synchronized to avoid possible
        // future problems if we ever have threads creating forms in parallel.
        myLayout = MockFormHelper.getLayout();

        formWidget = new AbsolutePanel();
        formWidget.setStylePrimaryName("ode-SimpleMockForm");

        // Initialize mock form UI by adding the phone bar and title bar.
        formWidget.add(new PhoneBar());
        titleBar = new TitleBar();
        formWidget.add(titleBar);

        // Put a ScrollPanel around the rootPanel.
        scrollPanel = new ScrollPanel(rootPanel);
        formWidget.add(scrollPanel);

        //Add navigation bar at the bottom of the viewer.
        formWidget.add(new NavigationBar());

        initComponent(formWidget);

        // Set up the initial state of the vertical alignment property editor and its dropdowns
        try {
            myVAlignmentPropertyEditor = PropertiesUtil.getVAlignmentEditor(properties);
        } catch (BadPropertyEditorException e) {
            OdeLog.log(MESSAGES.badAlignmentPropertyEditorForArrangement());
            return;
        }
        enableAndDisableDropdowns();
        initialized = true;
        // Now that the default for Scrollable is false, we need to force setting the property when creating the MockForm
        setScrollableProperty(getPropertyValue(PROPERTY_NAME_SCROLLABLE));
    }

    public void changePreviewSize(boolean isTablet) {
        if (isTablet) {
            PORTRAIT_WIDTH = TABLET_PORTRAIT_WIDTH;
            PORTRAIT_HEIGHT = TABLET_PORTRAIT_HEIGHT;
            LANDSCAPE_WIDTH = TABLET_LANDSCAPE_WIDTH;
            LANDSCAPE_HEIGHT = TABLET_LANDSCAPE_HEIGHT;
        } else {
            PORTRAIT_WIDTH = PHONE_PORTRAIT_WIDTH;
            PORTRAIT_HEIGHT = PHONE_PORTRAIT_HEIGHT;
            LANDSCAPE_WIDTH = PHONE_LANDSCAPE_WIDTH;
            LANDSCAPE_HEIGHT = PHONE_LANDSCAPE_HEIGHT;
        }

        if (landscape)
            resizePanel(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT);
        else
            resizePanel(PORTRAIT_WIDTH, PORTRAIT_HEIGHT);
    }

    /*
     * Resizes the scrollPanel and formWidget based on the screen size.
     */
    private void resizePanel(int newWidth, int newHeight) {
        screenWidth = newWidth;
        screenHeight = newHeight;
        usableScreenHeight = screenHeight - PhoneBar.HEIGHT - TitleBar.HEIGHT - NavigationBar.HEIGHT;

        rootPanel.setPixelSize(screenWidth, usableScreenHeight);
        scrollPanel.setPixelSize(screenWidth + getVerticalScrollbarWidth(), usableScreenHeight);
        formWidget.setPixelSize(screenWidth + getVerticalScrollbarWidth(), screenHeight);
        // Store properties
        changeProperty(PROPERTY_NAME_WIDTH, "" + screenWidth);
        boolean scrollable = Boolean.parseBoolean(getPropertyValue(PROPERTY_NAME_SCROLLABLE));
        if (!scrollable) {
            changeProperty(PROPERTY_NAME_HEIGHT, "" + usableScreenHeight);
        }
    }

    /*
    * Returns the width of a vertical scroll bar, calculating it if necessary.
    */
    private static int getVerticalScrollbarWidth() {
        // We only calculate the vertical scroll bar width once, then we store it in the static field
        // verticalScrollbarWidth. If the field is non-zero, we don't need to calculate it again.
        if (verticalScrollbarWidth == 0) {
            // The following code will calculate (on the fly) the width of a vertical scroll bar.
            // We'll create two divs, one inside the other and add the outer div to the document body,
            // but off-screen where the user won't see it.
            // We'll measure the width of the inner div twice: (first) when the outer div's vertical
            // scrollbar is hidden and (second) when the outer div's vertical scrollbar is visible.
            // The width of inner div will be smaller when outer div's vertical scrollbar is visible.
            // By subtracting the two measurements, we can calculate the width of the vertical scrollbar.

            // I used code from the following websites as reference material:
            // http://jdsharp.us/jQuery/minute/calculate-scrollbar-width.php
            // http://www.fleegix.org/articles/2006-05-30-getting-the-scrollbar-width-in-pixels

            Document document = Document.get();

            // Create an outer div.
            DivElement outerDiv = document.createDivElement();
            Style outerDivStyle = outerDiv.getStyle();
            // Use absolute positioning and set the top/left so that it is off-screen.
            // We don't want the user to see anything while we do this calculation.
            outerDivStyle.setProperty("position", "absolute");
            outerDivStyle.setProperty("top", "-1000px");
            outerDivStyle.setProperty("left", "-1000px");
            // Set the width and height of the outer div to a fixed size in pixels.
            outerDivStyle.setProperty("width", "100px");
            outerDivStyle.setProperty("height", "50px");
            // Hide the outer div's scrollbar by setting the "overflow" property to "hidden".
            outerDivStyle.setProperty("overflow", "hidden");

            // Create an inner div and put it inside the outer div.
            DivElement innerDiv = document.createDivElement();
            Style innerDivStyle = innerDiv.getStyle();
            // Set the height of the inner div to be 4 times the height of the outer div so that a
            // vertical scrollbar will be necessary (but hidden for now) on the outer div.
            innerDivStyle.setProperty("height", "200px");
            outerDiv.appendChild(innerDiv);

            // Temporarily add the outer div to the document body. It's off-screen so the user won't
            // actually see anything.
            Element bodyElement = document.getElementsByTagName("body").getItem(0);
            bodyElement.appendChild(outerDiv);

            // Get the width of the inner div while the outer div's vertical scrollbar is hidden.
            int widthWithoutScrollbar = innerDiv.getOffsetWidth();
            // Show the outer div's vertical scrollbar by setting the "overflow" property to "auto".
            outerDivStyle.setProperty("overflow", "auto");
            // Now, get the width of the inner div while the vertical scrollbar is visible.
            int widthWithScrollbar = innerDiv.getOffsetWidth();

            // Remove the outer div from the document body.
            bodyElement.removeChild(outerDiv);

            // Calculate the width of the vertical scrollbar by subtracting the two widths.
            verticalScrollbarWidth = widthWithoutScrollbar - widthWithScrollbar;
        }

        return verticalScrollbarWidth;
    }

    @Override
    public final MockForm getForm() {
        return this;
    }

    @Override
    public boolean isForm() {
        return true;
    }

    @Override
    public String getVisibleTypeName() {
        return VISIBLE_TYPE;
    }

    @Override
    protected void addWidthHeightProperties() {
        addProperty(PROPERTY_NAME_WIDTH, "" + PORTRAIT_WIDTH, null, new YoungAndroidLengthPropertyEditor());
        addProperty(PROPERTY_NAME_HEIGHT, "" + LENGTH_PREFERRED, null, new YoungAndroidLengthPropertyEditor());
    }

    @Override
    public boolean isPropertyPersisted(String propertyName) {
        // We use the Width and Height properties to make the form appear correctly in the designer,
        // but they aren't actually persisted to the .scm file.
        if (propertyName.equals(PROPERTY_NAME_WIDTH) || propertyName.equals(PROPERTY_NAME_HEIGHT)) {
            return false;
        }
        return super.isPropertyPersisted(propertyName);
    }

    @Override
    protected boolean isPropertyVisible(String propertyName) {
        if (propertyName.equals(PROPERTY_NAME_WIDTH) || propertyName.equals(PROPERTY_NAME_HEIGHT)) {
            return false;
        }

        if (propertyName.equals(PROPERTY_NAME_ICON)) {
            // The Icon property actually applies to the application and is only visible on Screen1.
            return editor.isScreen1();
        }

        if (propertyName.equals(PROPERTY_NAME_VNAME)) {
            // The VersionName property actually applies to the application and is only visible on Screen1.
            return editor.isScreen1();
        }

        if (propertyName.equals(PROPERTY_NAME_VCODE)) {
            // The VersionCode property actually applies to the application and is only visible on Screen1.
            return editor.isScreen1();
        }

        if (propertyName.equals(PROPERTY_NAME_SIZING)) {
            // The Sizing property actually applies to the application and is only visible on Screen1.
            return editor.isScreen1();
        }

        if (propertyName.equals(PROPERTY_NAME_ANAME)) {
            // The AppName property actually applies to the application and is only visible on Screen1.
            return editor.isScreen1();
        }

        return super.isPropertyVisible(propertyName);
    }

    /*
     * Sets the form's BackgroundColor property to a new value.
     */
    private void setBackgroundColorProperty(String text) {
        if (MockComponentsUtil.isNoneColor(text)) {
            text = "&HFF000000"; // black
        } else if (MockComponentsUtil.isDefaultColor(text)) {
            text = "&HFFFFFFFF"; // white
        }
        MockComponentsUtil.setWidgetBackgroundColor(rootPanel, text);
    }

    /*
     * Sets the form's BackgroundImage property to a new value.
     */
    private void setBackgroundImageProperty(String text) {
        String url = convertImagePropertyValueToUrl(text);
        if (url == null) {
            // text was not recognized as an asset.
            url = "";
        }
        MockComponentsUtil.setWidgetBackgroundImage(rootPanel, url);
    }

    private void setScreenOrientationProperty(String text) {
        if (hasProperty(PROPERTY_NAME_WIDTH) && hasProperty(PROPERTY_NAME_HEIGHT)
                && hasProperty(PROPERTY_NAME_SCROLLABLE)) {
            if (text.equalsIgnoreCase("landscape")) {
                screenWidth = LANDSCAPE_WIDTH;
                screenHeight = LANDSCAPE_HEIGHT;
                landscape = true;
            } else {
                screenWidth = PORTRAIT_WIDTH;
                screenHeight = PORTRAIT_HEIGHT;
                landscape = false;
            }
            usableScreenHeight = screenHeight - PhoneBar.HEIGHT - TitleBar.HEIGHT - NavigationBar.HEIGHT;
            resizePanel(screenWidth, screenHeight);

            changeProperty(PROPERTY_NAME_WIDTH, "" + screenWidth);
            boolean scrollable = Boolean.parseBoolean(getPropertyValue(PROPERTY_NAME_SCROLLABLE));
            if (!scrollable) {
                changeProperty(PROPERTY_NAME_HEIGHT, "" + usableScreenHeight);
            }
        }
    }

    private void setScrollableProperty(String text) {
        if (hasProperty(PROPERTY_NAME_HEIGHT)) {
            final boolean scrollable = Boolean.parseBoolean(text);
            int heightHint = scrollable ? LENGTH_PREFERRED : usableScreenHeight;
            changeProperty(PROPERTY_NAME_HEIGHT, "" + heightHint);
        }
    }

    private void setIconProperty(String icon) {
        // The Icon property actually applies to the application and is only visible on Screen1.
        // When we load a form that is not Screen1, this method will be called with the default value
        // for icon (empty string). We need to ignore that.
        if (editor.isScreen1()) {
            editor.getProjectEditor().changeProjectSettingsProperty(
                    SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_ICON,
                    icon);
        }
    }

    private void setVCodeProperty(String vcode) {
        // The VersionCode property actually applies to the application and is only visible on Screen1.
        // When we load a form that is not Screen1, this method will be called with the default value
        // for VersionCode (1). We need to ignore that.
        if (editor.isScreen1()) {
            editor.getProjectEditor().changeProjectSettingsProperty(
                    SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
                    SettingsConstants.YOUNG_ANDROID_SETTINGS_VERSION_CODE, vcode);
        }
    }

    private void setVNameProperty(String vname) {
        // The VersionName property actually applies to the application and is only visible on Screen1.
        // When we load a form that is not Screen1, this method will be called with the default value
        // for VersionName (1.0). We need to ignore that.
        if (editor.isScreen1()) {
            editor.getProjectEditor().changeProjectSettingsProperty(
                    SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
                    SettingsConstants.YOUNG_ANDROID_SETTINGS_VERSION_NAME, vname);
        }
    }

    private void setSizingProperty(String sizingProperty) {
        // The Compatibility property actually applies to the application and is only visible on
        // Screen1. When we load a form that is not Screen1, this method will be called with the
        // default value for CompatibilityProperty (false). We need to ignore that.
        if (editor.isScreen1()) {
            editor.getProjectEditor().changeProjectSettingsProperty(
                    SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
                    SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING, sizingProperty);
        }
    }

    private void setANameProperty(String aname) {
        // The AppName property actually applies to the application and is only visible on Screen1.
        // When we load a form that is not Screen1, this method will be called with the default value
        if (editor.isScreen1()) {
            editor.getProjectEditor().changeProjectSettingsProperty(
                    SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
                    SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME, aname);
        }
    }

    /**
     * Forces a re-layout of the child components of the container.
     */
    public final void refresh() {
        Map<MockComponent, LayoutInfo> layoutInfoMap = new HashMap<MockComponent, LayoutInfo>();

        collectLayoutInfos(layoutInfoMap, this);

        LayoutInfo formLayoutInfo = layoutInfoMap.get(this);
        layout.layoutChildren(formLayoutInfo);
        rootPanel.setPixelSize(formLayoutInfo.width, Math.max(formLayoutInfo.height, usableScreenHeight));

        for (LayoutInfo layoutInfo : layoutInfoMap.values()) {
            layoutInfo.cleanUp();
        }
        layoutInfoMap.clear();
    }

    /*
     * Collects the LayoutInfo of the given component and, recursively, all of
     * its children.
     *
     * If a component's width/height hint is automatic, the corresponding
     * LayoutInfo's width/height will be set to the calculated width/height.
     * If a component's width/height hint is fill parent, the corresponding
     * LayoutInfo's width/height may be set to fill parent. This will be resolved
     * when layoutChildren is called.
     */
    private static void collectLayoutInfos(Map<MockComponent, LayoutInfo> layoutInfoMap, MockComponent component) {

        LayoutInfo layoutInfo = component.createLayoutInfo(layoutInfoMap);

        // If this component is a container, collect the LayoutInfos of its children.
        if (component instanceof MockContainer) {
            if (!layoutInfo.visibleChildren.isEmpty()) {
                // We resize the container to be very large so that we get accurate
                // results when we ask for a child's size using getOffsetWidth/getOffsetHeight.
                // If the container is its normal size (or perhaps the default empty
                // size), then the browser won't give us anything bigger than that
                // when we ask for a child's size.
                if (component.isForm()) {
                    ((MockForm) component).rootPanel.setPixelSize(1000, 1000);
                } else {
                    component.setPixelSize(1000, 1000);
                }

                // Show children that should be shown and collect their layoutInfos.
                // Note that some MockLayout implementations may hide children that are in the
                // visibleChildren list. For example, in MockTableLayout, if two or more children occupy
                // the same cell in the table, all but one of the children are hidden.
                for (MockComponent child : layoutInfo.visibleChildren) {
                    child.setVisible(true);
                    collectLayoutInfos(layoutInfoMap, child);
                }
            }

            // Hide children that should be hidden.
            for (MockComponent child : component.getHiddenVisibleChildren()) {
                child.setVisible(false);
            }
        }

        layoutInfo.gatherDimensions();
    }

    /**
     * Adds an {@link FormChangeListener} to the listener set if it isn't already in there.
     *
     * @param listener  the {@code FormChangeListener} to be added
     */
    public void addFormChangeListener(FormChangeListener listener) {
        formChangeListeners.add(listener);
    }

    /**
     * Removes an {@link FormChangeListener} from the listener list.
     *
     * @param listener  the {@code FormChangeListener} to be removed
     */
    public void removeFormChangeListener(FormChangeListener listener) {
        formChangeListeners.remove(listener);
    }

    /**
     * Triggers a component property change event to be sent to the listener on the listener list.
     */
    protected void fireComponentPropertyChanged(MockComponent component, String propertyName,
            String propertyValue) {
        for (FormChangeListener listener : formChangeListeners) {
            listener.onComponentPropertyChanged(component, propertyName, propertyValue);
        }
    }

    /**
     * Triggers a component removed event to be sent to the listener on the listener list.
     */
    protected void fireComponentRemoved(MockComponent component, boolean permanentlyDeleted) {
        for (FormChangeListener listener : formChangeListeners) {
            listener.onComponentRemoved(component, permanentlyDeleted);
        }
    }

    /**
     * Triggers a component added event to be sent to the listener on the listener list.
     */
    protected void fireComponentAdded(MockComponent component) {
        for (FormChangeListener listener : formChangeListeners) {
            listener.onComponentAdded(component);
        }
    }

    /**
     * Triggers a component renamed event to be sent to the listener on the listener list.
     */
    protected void fireComponentRenamed(MockComponent component, String oldName) {
        for (FormChangeListener listener : formChangeListeners) {
            listener.onComponentRenamed(component, oldName);
        }
    }

    /**
     * Triggers a component selection change event to be sent to the listener on the listener list.
     */
    protected void fireComponentSelectionChange(MockComponent component, boolean selected) {
        for (FormChangeListener listener : formChangeListeners) {
            listener.onComponentSelectionChange(component, selected);
        }
    }

    /**
     * Changes the component that is currently selected in the form.
     * <p>
     * There will always be exactly one component selected in a form
     * at any given time.
     */
    public final void setSelectedComponent(MockComponent newSelectedComponent) {
        MockComponent oldSelectedComponent = selectedComponent;

        if (newSelectedComponent == null) {
            throw new IllegalArgumentException("at least one component must always be selected");
        }
        if (newSelectedComponent == oldSelectedComponent) {
            return;
        }

        selectedComponent = newSelectedComponent;

        if (oldSelectedComponent != null) { // Can be null initially
            oldSelectedComponent.onSelectedChange(false);
        }
        newSelectedComponent.onSelectedChange(true);
    }

    public final MockComponent getSelectedComponent() {
        return selectedComponent;
    }

    /**
     * Builds a tree of the component hierarchy of the form for display in the
     * {@code SourceStructureExplorer}.
     *
     * @return  tree showing the component hierarchy of the form
     */
    public TreeItem buildComponentsTree() {
        return buildTree();
    }

    // PropertyChangeListener implementation

    @Override
    public void onPropertyChange(String propertyName, String newValue) {
        super.onPropertyChange(propertyName, newValue);

        // Apply changed properties to the mock component
        if (propertyName.equals(PROPERTY_NAME_BACKGROUNDCOLOR)) {
            setBackgroundColorProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_BACKGROUNDIMAGE)) {
            setBackgroundImageProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_SCREEN_ORIENTATION)) {
            setScreenOrientationProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_SCROLLABLE)) {
            setScrollableProperty(newValue);
            adjustAlignmentDropdowns();
        } else if (propertyName.equals(PROPERTY_NAME_TITLE)) {
            titleBar.changeTitle(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_SIZING)) {
            if (newValue.equals("Fixed")) { // Disable Tablet Preview
                editor.getVisibleComponentsPanel().enableTabletPreviewCheckBox(false);
            } else {
                editor.getVisibleComponentsPanel().enableTabletPreviewCheckBox(true);
            }
            setSizingProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_ICON)) {
            setIconProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_VCODE)) {
            setVCodeProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_VNAME)) {
            setVNameProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_ANAME)) {
            setANameProperty(newValue);
        } else if (propertyName.equals(PROPERTY_NAME_HORIZONTAL_ALIGNMENT)) {
            myLayout.setHAlignmentFlags(newValue);
            refreshForm();
        } else if (propertyName.equals(PROPERTY_NAME_VERTICAL_ALIGNMENT)) {
            myLayout.setVAlignmentFlags(newValue);
            refreshForm();
        }
    }

    // enableAndDisable It should not be called until the component is initialized.
    // Otherwise, we'll get NPEs in trying to use myAlignmentPropertyEditor.
    private void adjustAlignmentDropdowns() {
        if (initialized)
            enableAndDisableDropdowns();
    }

    // Don't forget to call this on initialization!!!
    // If scrollable is True, the selector for vertical alignment should be disabled.
    private void enableAndDisableDropdowns() {
        String scrollable = properties.getProperty(PROPERTY_NAME_SCROLLABLE).getValue();
        if (scrollable.equals("True")) {
            myVAlignmentPropertyEditor.disable();
        } else {
            myVAlignmentPropertyEditor.enable();
        }
    }

    @Override
    public EditableProperties getProperties() {
        // Before we return the Properties object, we make sure that the
        // Sizing property has the value from the project's properties
        // this is because Sizing is per project, not per Screen(Form)
        // We only have to do this on screens other then screen1 because
        // screen1's value is definitive.
        if (!editor.isScreen1()) {
            properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING,
                    editor.getProjectEditor().getProjectSettingsProperty(
                            SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
                            SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING));
        }
        return properties;
    }

}