// -*- 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 java.util.HashMap; import java.util.HashSet; import java.util.List; 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.YaFormEditor; 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.components.common.ComponentConstants; 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.Timer; import com.google.gwt.user.client.ui.AbsolutePanel; import com.google.gwt.user.client.ui.Button; 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 TITLEBAR_HEIGHT = 24; private static final int ACTIONBAR_HEIGHT = 56; // UI elements private Label title; private Button menuButton; private AbsolutePanel bar; private boolean actionBar; private String backgroundColor; public String getTitle() { return title.getText(); } /* * Creates a new title bar. */ TitleBar() { title = new Label(); title.setStylePrimaryName("ode-SimpleMockFormTitle"); title.setHorizontalAlignment(Label.ALIGN_LEFT); menuButton = new Button(); menuButton.setText("\u22ee"); menuButton.setStylePrimaryName("ode-SimpleMockFormMenuButton"); bar = new AbsolutePanel(); bar.add(title); bar.add(menuButton); initWidget(bar); setStylePrimaryName("ode-SimpleMockFormTitleBar"); setSize("100%", TITLEBAR_HEIGHT + "px"); } /* * Changes the title in the title bar. */ void changeTitle(String newTitle) { title.setText(newTitle); } void setActionBar(boolean actionBar) { this.actionBar = actionBar; setSize("100%", (actionBar ? ACTIONBAR_HEIGHT : TITLEBAR_HEIGHT) + "px"); if (actionBar) { addStyleDependentName("ActionBar"); MockComponentsUtil.setWidgetBackgroundColor(titleBar.bar, backgroundColor); } else { removeStyleDependentName("ActionBar"); MockComponentsUtil.setWidgetBackgroundColor(titleBar.bar, "&HFF696969"); } } void setBackgroundColor(String color) { this.backgroundColor = color; if (actionBar) { MockComponentsUtil.setWidgetBackgroundColor(titleBar.bar, color); } } int getHeight() { return actionBar ? ACTIONBAR_HEIGHT : TITLEBAR_HEIGHT; } } /* * 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 = 44; // UI elements private AbsolutePanel bar; /* * Creates a new phone navigation bar; Shows at the bottom of the viewer. */ NavigationBar() { bar = new AbsolutePanel(); initWidget(bar); setStylePrimaryName("ode-SimpleMockFormNavigationBarPortrait"); } public int getHeight() { return HEIGHT; } } /** * 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; private int idxPhoneSize = 0; // 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 private static final String PROPERTY_NAME_TITLEVISIBLE = "TitleVisible"; // Don't show except on screen1 private static final String PROPERTY_NAME_SHOW_LISTS_AS_JSON = "ShowListsAsJson"; private static final String PROPERTY_NAME_TUTORIAL_URL = "TutorialURL"; private static final String PROPERTY_NAME_BLOCK_SUBSET = "BlocksToolkit"; private static final String PROPERTY_NAME_ACTIONBAR = "ActionBar"; private static final String PROPERTY_NAME_PRIMARY_COLOR = "PrimaryColor"; private static final String PROPERTY_NAME_PRIMARY_COLOR_DARK = "PrimaryColorDark"; private static final String PROPERTY_NAME_ACCENT_COLOR = "AccentColor"; private static final String PROPERTY_NAME_THEME = "Theme"; // Form UI components AbsolutePanel formWidget; AbsolutePanel phoneWidget; AbsolutePanel responsivePanel; ScrollPanel scrollPanel; private TitleBar titleBar; private NavigationBar navigationBar; 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 int usableScreenWidth; // 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(); phoneWidget = new AbsolutePanel(); phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhonePortrait"); formWidget = new AbsolutePanel(); formWidget.setStylePrimaryName("ode-SimpleMockForm"); responsivePanel = new AbsolutePanel(); // Initialize mock form UI by adding the phone bar and title bar. responsivePanel.add(new PhoneBar()); titleBar = new TitleBar(); responsivePanel.add(titleBar); // Put a ScrollPanel around the rootPanel. scrollPanel = new ScrollPanel(rootPanel); responsivePanel.add(scrollPanel); formWidget.add(responsivePanel); //Add navigation bar at the bottom of the viewer. navigationBar = new NavigationBar(); formWidget.add(navigationBar); phoneWidget.add(formWidget); initComponent(phoneWidget); // 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(int width, int height, int idx) { // It will definitely be modified in the future to add more options. PORTRAIT_WIDTH = width; PORTRAIT_HEIGHT = height; LANDSCAPE_WIDTH = height; LANDSCAPE_HEIGHT = width; idxPhoneSize = idx; setPhoneStyle(); if (landscape) { resizePanel(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT); } else { resizePanel(width, height); } } private void setPhoneStyle() { if (landscape) { if (idxPhoneSize == 0) phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhoneLandscape"); else if (idxPhoneSize == 1) phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhoneLandscapeTablet"); else if (idxPhoneSize == 2) phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhoneLandscapeMonitor"); navigationBar.setStylePrimaryName("ode-SimpleMockFormNavigationBarLandscape"); } else { if (idxPhoneSize == 0) phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhonePortrait"); else if (idxPhoneSize == 1) phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhonePortraitTablet"); else if (idxPhoneSize == 2) phoneWidget.setStylePrimaryName("ode-SimpleMockFormPhonePortraitMonitor"); navigationBar.setStylePrimaryName("ode-SimpleMockFormNavigationBarPortrait"); } } /* * Resizes the scrollPanel, responsivePanel, and formWidget based on the screen size. */ private void resizePanel(int newWidth, int newHeight){ screenWidth = newWidth; screenHeight = newHeight; if (landscape) { usableScreenWidth = screenWidth - navigationBar.getHeight(); usableScreenHeight = screenHeight - PhoneBar.HEIGHT - titleBar.getHeight(); } else { usableScreenWidth = screenWidth; usableScreenHeight = screenHeight - PhoneBar.HEIGHT - titleBar.getHeight() - navigationBar.getHeight(); } rootPanel.setPixelSize(usableScreenWidth, usableScreenHeight); scrollPanel.setPixelSize(usableScreenWidth + getVerticalScrollbarWidth(), usableScreenHeight); formWidget.setPixelSize(screenWidth + getVerticalScrollbarWidth(), screenHeight); // Store properties changeProperty(PROPERTY_NAME_WIDTH, "" + usableScreenWidth); 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) { switch (propertyName) { case PROPERTY_NAME_WIDTH: case PROPERTY_NAME_HEIGHT: case PROPERTY_NAME_ACTIONBAR: { return false; } // The Icon property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_ICON: // The VersionName property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_VNAME: // The VersionCode property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_VCODE: // The Sizing property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_SIZING: // The AppName property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_ANAME: // The ShowListsAsJson property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_SHOW_LISTS_AS_JSON: // The TutorialURL property actually applies to the application and is only visible on Screen1. case PROPERTY_NAME_TUTORIAL_URL: case PROPERTY_NAME_BLOCK_SUBSET: case PROPERTY_NAME_PRIMARY_COLOR: case PROPERTY_NAME_PRIMARY_COLOR_DARK: case PROPERTY_NAME_ACCENT_COLOR: case PROPERTY_NAME_THEME: { return editor.isScreen1(); } default: { 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; } setPhoneStyle(); if (landscape) { usableScreenWidth = screenWidth - navigationBar.getHeight(); usableScreenHeight = screenHeight - PhoneBar.HEIGHT - titleBar.getHeight(); } else { usableScreenWidth = screenWidth; usableScreenHeight = screenHeight - PhoneBar.HEIGHT - titleBar.getHeight() - navigationBar.getHeight(); } resizePanel(screenWidth, screenHeight); changeProperty(PROPERTY_NAME_WIDTH, "" + usableScreenWidth); 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 setShowListsAsJsonProperty(String asJson) { // This 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 ShowListsAsJsonProperty (false). We need to ignore that. if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON, asJson); } } private void setTutorialURLProperty(String asJson) { // This 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 TutorialURL (""). We need to ignore that. if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL, asJson); } } private void setBlockSubsetProperty(String asJson) { //This property 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 SubsetJson (""). We need to ignore that. if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_BLOCK_SUBSET, asJson); if (editor.isLoadComplete()) { ((YaFormEditor)editor).reloadComponentPalette(asJson); } } } 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); } } private void setTitleVisibleProperty(String text) { boolean visible = Boolean.parseBoolean(text); titleBar.setVisible(visible); } private void setActionBarProperty(String actionBar) { if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_ACTIONBAR, actionBar); } titleBar.setActionBar(Boolean.parseBoolean(actionBar)); if (initialized) { resizePanel(screenWidth, screenHeight); // update screen due to titlebar size change. } } private void setPrimaryColor(String color) { if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_PRIMARY_COLOR, color); } if (color.equals("&H00000000")) { // Replace Default with actual default color color = ComponentConstants.DEFAULT_PRIMARY_COLOR; } titleBar.setBackgroundColor(color); } private void setPrimaryColorDark(String color) { if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_PRIMARY_COLOR_DARK, color); } } private void setAccentColor(String color) { if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_ACCENT_COLOR, color); } } private void setTheme(String theme) { if (editor.isScreen1()) { editor.getProjectEditor().changeProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_THEME, theme); } if (theme.equals("AppTheme.Light")) { final String newColor = "&HFF000000"; MockComponentsUtil.setWidgetTextColor(titleBar.bar, newColor); MockComponentsUtil.setWidgetTextColor(titleBar.menuButton, newColor); MockComponentsUtil.setWidgetTextColor(titleBar.title, newColor); } else { final String newColor = "&HFFFFFFFF"; MockComponentsUtil.setWidgetTextColor(titleBar.bar, newColor); MockComponentsUtil.setWidgetTextColor(titleBar.menuButton, newColor); MockComponentsUtil.setWidgetTextColor(titleBar.title, newColor); } if (theme.equals("AppTheme")) { formWidget.addStyleDependentName("Dark"); } else { formWidget.removeStyleDependentName("Dark"); } } /** * Forces a re-layout of the child components of the container. * * Each components onPropertyChange listener calls us. This is * reasonable during interactive editing because we have to make * sure the screen reflects what the user is doing. However during * project load we will be called many times, when really we should * only be called after the project's UI is really finished loading. * * We could add a bunch of complicated code to inhibit refreshes * until we know the project's UI is loaded and stable. However that * is a change that will be spread over several modules, making it * hard to understand what is going on. * * Instead, I am opting to keep this change self contained within * this module. The idea is to see how quickly we are being * called. If we receive a call which is close in time (within * seconds) of a previous call, we set a timer to fire in the * reasonable future (say 2 seconds). While this timer is counting * down, we ignore any other calls to refresh. Whatever refreshing * they would do will be handled by the call done when the timer * fires. This approach does not reduce the number of calls to * refresh during project loading to 1. But it significantly reduces * the number of calls and gets us out of the exponential explosion * in time and memory that we see with projects with hundreds of * design elements (yes, people do that, and I have seen at least * one project that was this big and reasonable!). -Jeff Schiller * ([email protected]). * */ private Timer refreshTimer = null; public final void refresh() { if (refreshTimer != null) return; refreshTimer = new Timer() { @Override public void run() { doRefresh(); refreshTimer = null; } }; refreshTimer.schedule(0); } /* * Do the actual refresh. * * This method is public because it is called directly from MockComponent for refreshes * which bypass throttling. * */ public final void doRefresh() { 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"); } YaFormEditor formEditor = (YaFormEditor) editor; boolean shouldSelectMultipleComponents = formEditor.getShouldSelectMultipleComponents(); List<MockComponent> selectedComponents = formEditor.getSelectedComponents(); if (selectedComponents.size() == 1 && selectedComponents.contains(newSelectedComponent)) { // Attempting to change the selection from old to new when they are the same breaks // Marker drag. See https://github.com/mit-cml/appinventor-sources/issues/1936 return; } if (shouldSelectMultipleComponents && selectedComponents.size() > 1 && formEditor.isSelectedComponent(newSelectedComponent)) { int index = selectedComponents.indexOf(newSelectedComponent); selectedComponent = selectedComponents.get((index == 0) ? 1 : index - 1); newSelectedComponent.onSelectedChange(false); return; } selectedComponent = newSelectedComponent; Map<String, MockComponent> componentsMap = formEditor.getComponents(); if (oldSelectedComponent != null && !shouldSelectMultipleComponents) { // Can be null initially for (MockComponent component : componentsMap.values()) { if (component.getName() != selectedComponent.getName()) { component.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_SHOW_LISTS_AS_JSON)) { setShowListsAsJsonProperty(newValue); } else if (propertyName.equals(PROPERTY_NAME_TUTORIAL_URL)) { setTutorialURLProperty(newValue); } else if (propertyName.equals(PROPERTY_NAME_BLOCK_SUBSET)) { setBlockSubsetProperty(newValue); } else if (propertyName.equals(PROPERTY_NAME_ACTIONBAR)) { setActionBarProperty(newValue); } else if (propertyName.equals(PROPERTY_NAME_THEME)) { setTheme(newValue); if ("Classic".equals(newValue)) { getProperties().getExistingProperty(PROPERTY_NAME_ACTIONBAR).setValue("False"); } else { getProperties().getExistingProperty(PROPERTY_NAME_ACTIONBAR).setValue("True"); } } else if (propertyName.equals(PROPERTY_NAME_PRIMARY_COLOR)) { setPrimaryColor(newValue); } else if (propertyName.equals(PROPERTY_NAME_PRIMARY_COLOR_DARK)) { setPrimaryColorDark(newValue); } else if (propertyName.equals(PROPERTY_NAME_ACCENT_COLOR)) { setAccentColor(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(); } else if (propertyName.equals(PROPERTY_NAME_TITLEVISIBLE)) { setTitleVisibleProperty(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, ShowListsAsJson and TutorialURL properties have the // value from the project's properties this is because these are // 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)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_BLOCK_SUBSET, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_BLOCK_SUBSET)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_ACTIONBAR, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_ACTIONBAR)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_THEME, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_THEME)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_PRIMARY_COLOR, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_PRIMARY_COLOR)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_PRIMARY_COLOR_DARK, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_PRIMARY_COLOR_DARK)); properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_ACCENT_COLOR, editor.getProjectEditor().getProjectSettingsProperty( SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_ACCENT_COLOR)); } return properties; } }