Add a TutorialURL property

The TutorialURL will be opened in a sidebar in MIT App Inventor. This
permits people to build “template” projects with a tutorial (hosted on
their own site) displayed in a sidebar.

For now, we only allow tutorial urls from http://appinventor.mit.edu/ or
http://appinv.us/.

Change-Id: Ia79eeac6bd3809ce32d0475aa3954ad2c996c776
parent 6497ac17
......@@ -111,6 +111,7 @@ public class DesignToolbar extends Toolbar {
}
}
private static final String WIDGET_NAME_TUTORIAL_TOGGLE = "TutorialToggle";
private static final String WIDGET_NAME_ADDFORM = "AddForm";
private static final String WIDGET_NAME_REMOVEFORM = "RemoveForm";
private static final String WIDGET_NAME_SCREENS_DROPDOWN = "ScreensDropdown";
......@@ -166,6 +167,10 @@ public class DesignToolbar extends Toolbar {
// width of palette minus cellspacing/border of buttons
toolbar.setCellWidth(projectNameLabel, "222px");
addButton(new ToolbarItem(WIDGET_NAME_TUTORIAL_TOGGLE,
MESSAGES.toggleTutorialButton(), new ToogleTutorialAction()));
setButtonVisible(WIDGET_NAME_TUTORIAL_TOGGLE, false); // Don't show unless needed
List<DropDownItem> screenItems = Lists.newArrayList();
addDropDownButton(WIDGET_NAME_SCREENS_DROPDOWN, MESSAGES.screensButton(), screenItems);
......@@ -186,6 +191,19 @@ public class DesignToolbar extends Toolbar {
Ode.getInstance().getTopToolbar().updateFileMenuButtons(0);
}
private class ToogleTutorialAction implements Command {
@Override
public void execute() {
Ode ode = Ode.getInstance();
boolean visible = ode.isTutorialVisible();
if (visible) {
ode.setTutorialVisible(false);
} else {
ode.setTutorialVisible(true);
}
}
}
private class AddFormAction implements Command {
@Override
public void execute() {
......@@ -518,4 +536,12 @@ public class DesignToolbar extends Toolbar {
return currentView;
}
public void setTutorialToggleVisible(boolean value) {
if (value) {
setButtonVisible(WIDGET_NAME_TUTORIAL_TOGGLE, true);
} else {
setButtonVisible(WIDGET_NAME_TUTORIAL_TOGGLE, false);
}
}
}
......@@ -109,6 +109,7 @@ import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Frame;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
......@@ -207,15 +208,17 @@ public class Ode implements EntryPoint {
* |+-- topPanel -------------------------------+|
* || ||
* |+-------------------------------------------+|
* |+-- deckPanel ------------------------------+|
* || ||
* |+-------------------------------------------+|
* |+-- overDeckPanel --+-----------------------+|
* || tutorialPanel | deckPanel ||
* |+-------------------+-----------------------+|
* |+-- statusPanel ----------------------------+|
* || ||
* |+-------------------------------------------+|
* +---------------------------------------------+
*/
private DeckPanel deckPanel;
private HorizontalPanel overDeckPanel;
private Frame tutorialPanel;
private int projectsTabIndex;
private int designTabIndex;
private int debuggingTabIndex;
......@@ -236,6 +239,9 @@ public class Ode implements EntryPoint {
private DesignToolbar designToolbar;
private TopToolbar topToolbar;
// Is the tutorial toolbar currently displayed?
private boolean tutorialVisible = false;
// Popup that indicates that an asynchronous request is pending. It is visible
// initially, and will be hidden automatically after the first RPC completes.
private static RpcStatusPopup rpcStatusPopup;
......@@ -410,6 +416,7 @@ public class Ode implements EntryPoint {
public void switchToProjectsView() {
// We may need to pass the code below as a runnable to
// screenShotMaybe() so build the runnable now
hideTutorials();
Runnable next = new Runnable() {
@Override
public void run() {
......@@ -440,6 +447,7 @@ public class Ode implements EntryPoint {
*/
public void switchToUserAdminPanel() {
hideTutorials();
currentView = USERADMIN;
deckPanel.showWidget(userAdminTabIndex);
}
......@@ -448,6 +456,7 @@ public class Ode implements EntryPoint {
* Switch to the Gallery tab
*/
public void switchToGalleryView() {
hideTutorials();
if (!galleryInitialized) {
// Gallery initialization is deferred until now.
initializeGallery();
......@@ -460,6 +469,7 @@ public class Ode implements EntryPoint {
* Switch to the Gallery App
*/
public void switchToGalleryAppView(GalleryApp app, int editStatus) {
hideTutorials();
if (!galleryInitialized) {
// Gallery initialization is deferred until now.
initializeGallery();
......@@ -474,6 +484,7 @@ public class Ode implements EntryPoint {
* TODO: change string parameter
*/
public void switchToUserProfileView(String userId, int editStatus) {
hideTutorials();
currentView = USERPROFILE;
OdeLog.log("###########" + userId + "||||||" + editStatus);
ProfileBox.setProfile(userId, editStatus);
......@@ -486,6 +497,7 @@ public class Ode implements EntryPoint {
public void switchToDesignView() {
// Only show designer if there is a current editor.
// ***** THE DESIGNER TAB DOES NOT DISPLAY CORRECTLY IF THERE IS NO CURRENT EDITOR. *****
showTutorials();
currentView = DESIGNER;
getTopToolbar().updateFileMenuButtons(currentView);
if (currentFileEditor != null) {
......@@ -508,6 +520,7 @@ public class Ode implements EntryPoint {
* Switch to the Moderation Page tab
*/
public void switchToModerationPageView() {
hideTutorials();
if (!galleryInitialized) {
initializeGallery();
}
......@@ -518,6 +531,7 @@ public class Ode implements EntryPoint {
* Switch to the Debugging tab
*/
public void switchToDebuggingView() {
hideTutorials();
deckPanel.showWidget(debuggingTabIndex);
// NOTE(lizlooney) - Calling resizeWorkArea for debuggingTab prevents the
......@@ -908,6 +922,15 @@ public class Ode implements EntryPoint {
DockPanel mainPanel = new DockPanel();
mainPanel.add(topPanel, DockPanel.NORTH);
// Create the Tutorial Panel
tutorialPanel = new Frame("");
tutorialPanel.setWidth("100%");
tutorialPanel.setHeight("100%");
// Initially we do not display it. If the project we load has
// a tutorial URL, then we will set this visible when we load
// the project
tutorialPanel.setVisible(false);
// Create tab panel for subsequent tabs
deckPanel = new DeckPanel() {
@Override
......@@ -1122,9 +1145,15 @@ public class Ode implements EntryPoint {
// ***** THE DESIGNER TAB DOES NOT DISPLAY CORRECTLY IF THERE IS NO CURRENT PROJECT. *****
deckPanel.showWidget(projectsTabIndex);
mainPanel.add(deckPanel, DockPanel.CENTER);
mainPanel.setCellHeight(deckPanel, "100%");
mainPanel.setCellWidth(deckPanel, "100%");
overDeckPanel = new HorizontalPanel();
overDeckPanel.setHeight("100%");
overDeckPanel.setWidth("100%");
overDeckPanel.add(tutorialPanel);
overDeckPanel.setCellWidth(tutorialPanel, "0%");
overDeckPanel.add(deckPanel);
mainPanel.add(overDeckPanel, DockPanel.CENTER);
mainPanel.setCellHeight(overDeckPanel, "100%");
mainPanel.setCellWidth(overDeckPanel, "100%");
// mainPanel.add(switchToDesignerButton, DockPanel.WEST);
// mainPanel.add(switchToBlocksButton, DockPanel.EAST);
......@@ -2322,6 +2351,59 @@ public class Ode implements EntryPoint {
return container;
}
// Used internally here so that the tutorial panel is only shown on
// the blocks or designer view, not the gallery or projects (or
// other) views. unlike setTutorialVisible, we do not side effect
// the instance variable tutorialVisible, so we can use it in showTutorials()
// (below) to put back the tutorial frame when we revisit the project
private void hideTutorials() {
tutorialPanel.setVisible(false);
overDeckPanel.setCellWidth(tutorialPanel, "0%");
}
private void showTutorials() {
if (tutorialVisible) {
tutorialPanel.setVisible(true);
}
}
public void setTutorialVisible(boolean visible) {
tutorialVisible = visible;
if (visible) {
tutorialPanel.setVisible(true);
tutorialPanel.setWidth("300px");
} else {
tutorialPanel.setVisible(false);
overDeckPanel.setCellWidth(tutorialPanel, "0%");
}
}
/**
* Indicate if the tutorial panel is currently visible.
* @return true if the tutorial panel is visible.
*
* Note: This value is only valid if in the blocks editor or the designer.
* As of this note this routine is called when the "Toogle Tutorial" button
* is clicked, and it is only displayed when in the Designer of the Blocks
* Editor.
*/
public boolean isTutorialVisible() {
return tutorialVisible;
}
public void setTutorialURL(String newURL) {
if (newURL.isEmpty() || (!newURL.startsWith("http://appinventor.mit.edu/")
&& !newURL.startsWith("http://appinv.us/"))) {
designToolbar.setTutorialToggleVisible(false);
setTutorialVisible(false);
} else {
tutorialPanel.setUrl(newURL);
designToolbar.setTutorialToggleVisible(true);
setTutorialVisible(true);
}
}
// Native code to set the top level rendezvousServer variable
// where blockly code can easily find it.
private native void setRendezvousServer(String server) /*-{
......
......@@ -102,6 +102,10 @@ public interface OdeMessages extends Messages {
@Description("Label of the button for checkpoint")
String checkpointButton();
@DefaultMessage("Toggle Tutorial")
@Description("Label for the Toggle Tutorial Button")
String toggleTutorialButton();
@DefaultMessage("Add Screen ...")
@Description("Label of the button for adding a new screen")
String addFormButton();
......@@ -3036,6 +3040,10 @@ public interface OdeMessages extends Messages {
@Description("")
String VersionNameProperties();
@DefaultMessage("TutorialURL")
@Description("")
String TutorialURLProperties();
@DefaultMessage("Sizing")
@Description("")
String SizingProperties();
......
......@@ -235,8 +235,15 @@ public abstract class ProjectEditor extends Composite {
Settings settings = projectSettings.getSettings(category);
String currentValue = settings.getPropertyValue(name);
if (!newValue.equals(currentValue)) {
OdeLog.log("ProjectEditor: changeProjectSettingsProperty: " + name + " " + currentValue +
" => " + newValue);
settings.changePropertyValue(name, newValue);
Ode.getInstance().getEditorManager().scheduleAutoSave(projectSettings);
// Deal with the Tutorial Panel
Ode ode = Ode.getInstance();
if (name.equals("TutorialURL")) {
ode.setTutorialURL(newValue);
}
ode.getEditorManager().scheduleAutoSave(projectSettings);
}
}
......@@ -308,6 +315,12 @@ public abstract class ProjectEditor extends Composite {
// project just after the editor is created.
OdeLog.log("ProjectEditor: got onLoad for project " + projectId);
super.onLoad();
String tutorialURL = getProjectSettingsProperty(SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL);
if (!tutorialURL.isEmpty()) {
Ode ode = Ode.getInstance();
ode.setTutorialURL(tutorialURL);
}
onShow();
}
......@@ -315,6 +328,9 @@ public abstract class ProjectEditor extends Composite {
@Override
protected void onUnload() {
// onUnload is called immediately before a widget becomes detached from the browser's document.
Ode ode = Ode.getInstance();
ode.setTutorialVisible(false);
ode.getDesignToolbar().setTutorialToggleVisible(false);
OdeLog.log("ProjectEditor: got onUnload for project " + projectId);
super.onUnload();
onHide();
......
......@@ -177,6 +177,7 @@ public final class MockForm extends MockContainer {
private static final String PROPERTY_NAME_SIZING = "Sizing"; // Don't show except on screen1
// 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";
// Form UI components
AbsolutePanel formWidget;
......@@ -429,6 +430,11 @@ public final class MockForm extends MockContainer {
return editor.isScreen1();
}
if (propertyName.equals(PROPERTY_NAME_TUTORIAL_URL)) {
// The TutorialURL property actually applies to the application and is only visible on Screen1.
return editor.isScreen1();
}
return super.isPropertyVisible(propertyName);
}
......@@ -534,7 +540,7 @@ public final class MockForm extends MockContainer {
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 CompatibilityProperty (false). We need to ignore that.
// default value for ShowListsAsJsonProperty (false). We need to ignore that.
if (editor.isScreen1()) {
editor.getProjectEditor().changeProjectSettingsProperty(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
......@@ -542,6 +548,17 @@ public final class MockForm extends MockContainer {
}
}
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 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
......@@ -821,6 +838,8 @@ public final class MockForm extends MockContainer {
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_HORIZONTAL_ALIGNMENT)) {
myLayout.setHAlignmentFlags(newValue);
refreshForm();
......@@ -850,20 +869,24 @@ public final class MockForm extends MockContainer {
@Override
public EditableProperties getProperties() {
// Before we return the Properties object, we make sure that the
// Sizing and ShowListsAsJson 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.
// 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));
// new code to test
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));
}
return properties;
}
......
......@@ -51,6 +51,9 @@ public final class YoungAndroidSettings extends Settings {
addProperty(new EditableProperty(this,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON, "false",
EditableProperty.TYPE_INVISIBLE));
addProperty(new EditableProperty(this,
SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL, "",
EditableProperty.TYPE_INVISIBLE));
}
}
......@@ -102,7 +102,7 @@ public final class FileImporterImpl implements FileImporter {
// so that it contains the correct entries for "main" and "name", which are dependent on
// the projectName and qualifiedFormName.
String content = YoungAndroidProjectService.getProjectPropertiesFileContents(
projectName, qualifiedFormName, null, null, null, null, null, null, null);
projectName, qualifiedFormName, null, null, null, null, null, null, null, null);
project.addTextFile(new TextFile(fileName, content));
isProjectArchive = true;
......@@ -149,7 +149,7 @@ public final class FileImporterImpl implements FileImporter {
if (projectHistory != null) {
project.setProjectHistory(projectHistory);
}
String settings = YoungAndroidProjectService.getProjectSettings(null, null, null, null, null, null, null);
String settings = YoungAndroidProjectService.getProjectSettings(null, null, null, null, null, null, null, null);
long projectId = storageIo.createProject(userId, project, settings);
return storageIo.getUserProject(userId, projectId);
}
......
......@@ -129,7 +129,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
* Returns project settings that can be used when creating a new project.
*/
public static String getProjectSettings(String icon, String vCode, String vName,
String useslocation, String aName, String sizing, String showListsAsJson) {
String useslocation, String aName, String sizing, String showListsAsJson, String tutorialURL) {
icon = Strings.nullToEmpty(icon);
vCode = Strings.nullToEmpty(vCode);
vName = Strings.nullToEmpty(vName);
......@@ -137,6 +137,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
sizing = Strings.nullToEmpty(sizing);
aName = Strings.nullToEmpty(aName);
showListsAsJson = Strings.nullToEmpty(showListsAsJson);
tutorialURL = Strings.nullToEmpty(tutorialURL);
return "{\"" + SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS + "\":{" +
"\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_ICON + "\":\"" + icon +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_VERSION_CODE + "\":\"" + vCode +
......@@ -145,6 +146,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME + "\":\"" + aName +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING + "\":\"" + sizing +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON + "\":\"" + showListsAsJson +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL + "\":\"" + tutorialURL +
"\"}}";
}
......@@ -159,7 +161,8 @@ public final class YoungAndroidProjectService extends CommonProjectService {
* @param vname the version name
*/
public static String getProjectPropertiesFileContents(String projectName, String qualifiedName,
String icon, String vcode, String vname, String useslocation, String aname, String sizing, String showListsAsJson) {
String icon, String vcode, String vname, String useslocation, String aname,
String sizing, String showListsAsJson, String tutorialURL) {
String contents = "main=" + qualifiedName + "\n" +
"name=" + projectName + '\n' +
"assets=../" + ASSETS_FOLDER + "\n" +
......@@ -186,6 +189,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
if (showListsAsJson != null && !showListsAsJson.isEmpty()) {
contents += "showlistsasjson=" + showListsAsJson + "\n";
}
if (tutorialURL != null && !tutorialURL.isEmpty()) {
contents += "tutorialurl=" + tutorialURL + "\n";
}
return contents;
}
......@@ -255,6 +261,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String newShowListsAsJson = Strings.nullToEmpty(settings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON));
String newTutorialURL = Strings.nullToEmpty(settings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL));
String newAName = Strings.nullToEmpty(settings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME));
......@@ -277,16 +286,18 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String oldSizing = Strings.nullToEmpty(properties.getProperty("sizing"));
String oldAName = Strings.nullToEmpty(properties.getProperty("aname"));
String oldShowListsAsJson = Strings.nullToEmpty(properties.getProperty("showlistsasjson"));
String oldTutorialURL = Strings.nullToEmpty(properties.getProperty("tutorialurl"));
if (!newIcon.equals(oldIcon) || !newVCode.equals(oldVCode) || !newVName.equals(oldVName)
|| !newUsesLocation.equals(oldUsesLocation) ||
!newAName.equals(oldAName) || !newSizing.equals(oldSizing) ||
!newShowListsAsJson.equals(oldShowListsAsJson)) {
!newShowListsAsJson.equals(oldShowListsAsJson) ||
!newTutorialURL.equals(oldTutorialURL)) {
// Recreate the project.properties and upload it to storageIo.
String projectName = properties.getProperty("name");
String qualifiedName = properties.getProperty("main");
String newContent = getProjectPropertiesFileContents(projectName, qualifiedName, newIcon,
newVCode, newVName, newUsesLocation, newAName, newSizing, newShowListsAsJson);
newVCode, newVName, newUsesLocation, newAName, newSizing, newShowListsAsJson, newTutorialURL);
storageIo.uploadFileForce(projectId, PROJECT_PROPERTIES_FILE_NAME, userId,
newContent, StorageUtil.DEFAULT_CHARSET);
}
......@@ -305,7 +316,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String propertiesFileName = PROJECT_PROPERTIES_FILE_NAME;
String propertiesFileContents = getProjectPropertiesFileContents(projectName,
qualifiedFormName, null, null, null, null, null, null, null);
qualifiedFormName, null, null, null, null, null, null, null, null);
String formFileName = YoungAndroidFormNode.getFormFileId(qualifiedFormName);
String formFileContents = getInitialFormPropertiesFileContents(qualifiedFormName);
......@@ -325,7 +336,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
project.addTextFile(new TextFile(yailFileName, yailFileContents));
// Create new project
return storageIo.createProject(userId, project, getProjectSettings("", "1", "1.0", "false", projectName, "Fixed", "false"));
return storageIo.createProject(userId, project, getProjectSettings("", "1", "1.0", "false", projectName, "Fixed", "false", ""));
}
@Override
......@@ -355,6 +366,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String showListsAsJson = oldSettings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON);
String tutorialURL = oldSettings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL);
Project newProject = new Project(newName);
newProject.setProjectType(YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE);
......@@ -374,7 +388,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String qualifiedFormName = StringUtils.getQualifiedFormName(
storageIo.getUser(userId).getUserEmail(), newName);
newContents = getProjectPropertiesFileContents(newName, qualifiedFormName, icon, vcode,
vname, useslocation, aname, sizing, showListsAsJson);
vname, useslocation, aname, sizing, showListsAsJson, tutorialURL);
} else {
// This is some file other than the project properties file.
// oldSourceFileName may contain the old project name as a path segment, surrounded by /.
......@@ -398,7 +412,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
// Create the new project and return the new project's id.
return storageIo.createProject(userId, newProject, getProjectSettings(icon, vcode, vname,
useslocation, aname, sizing, showListsAsJson));
useslocation, aname, sizing, showListsAsJson, tutorialURL));
}
@Override
......
......@@ -50,6 +50,7 @@ public class SettingsConstants {
public static final String YOUNG_ANDROID_SETTINGS_SIZING = "Sizing";
public static final String YOUNG_ANDROID_SETTINGS_APP_NAME = "AppName";
public static final String YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON = "ShowListsAsJson";
public static final String YOUNG_ANDROID_SETTINGS_TUTORIAL_URL = "TutorialURL";
/**
* Settings for the Blocks editor.
......
......@@ -492,7 +492,8 @@ public class ProjectServiceTest {
SettingsConstants.YOUNG_ANDROID_SETTINGS_USES_LOCATION + "\":\"false\",\"" +
SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME + "\":\"Project1\",\"" +
SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING + "\":\"Fixed\",\"" +
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON + "\":\"false\"}}",
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON + "\":\"false\",\"" +
SettingsConstants.YOUNG_ANDROID_SETTINGS_TUTORIAL_URL + "\":\"\"}}",
loadedSettings);
String storedSettings =
......
......@@ -371,11 +371,17 @@ Blockly.Yail.getFormPropertiesLines = function(formName, componentJson, includeC
* @param {Blockly.ComponentDatabase} componentDb The workspace's database of components and types.
* @returns {Array} code strings
* @private
*
* Hack Note (JIS): We do not output a property setter line for the TutorialURL
* property. This property is only for use within the designer and has no meaning
* within an Android app. It is harmless to output it, once we have deployed a new
* companion (version > 2.41). Once such a Companion is deployed, the exception
* for TutorialURL below (and this comment) can be removed.
*/
Blockly.Yail.getPropertySettersLines = function(componentJson, componentName, componentDb) {
var code = [];
for (var prop in componentJson) {
if (prop.charAt(0) != "$" && prop != "Uuid") {
if (prop.charAt(0) != "$" && prop != "Uuid" && prop != "TutorialURL") {
code.push(Blockly.Yail.getPropertySetterString(componentName, componentJson.$Type, prop,
componentJson[prop], componentDb));
}
......
......@@ -1488,6 +1488,16 @@ public class Form extends Activity
return formHeight;
}
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
defaultValue = "")
@SimpleProperty(userVisible = false,
description = "A URL to use to populate the Tutorial Sidebar while "
+ "editing a project. Used as a teaching aid.")
public void TutorialURL(String url) {
// We don't actually do anything This property is stored in the
// project properties file
}
/**
* Display a new form.
*
......
......@@ -988,6 +988,15 @@ none
<dd>The title bar is the top gray bar on the screen. This property reports whether the title bar is visible.</dd>
<dt><code>Title</code></dt>
<dd>The caption for the form, which apears in the title bar</dd>
<dt><code>TutorialURL</code></dt>
<dd>
A URL which will be opened on the left side panel (which can be
toggled once it is open). This is intended for projects that have
an in-line tutorial as part of the project. For security reasons,
only tutorials hosted on http://appinventor.mit.edu or linked to
from our URL shortener (http://appinv.us) may be used here. Other
URLs will be silently ignored.
</dd>
<dt><code>VersionCode</code> (designer only)</dt>
<dd>An integer value which must be incremented each time a new Android Application Package File (APK) is created for the Google Play Store.</dd>
<dt><code>VersionName</code> (designer only)</dt>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment