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 { ...@@ -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_ADDFORM = "AddForm";
private static final String WIDGET_NAME_REMOVEFORM = "RemoveForm"; private static final String WIDGET_NAME_REMOVEFORM = "RemoveForm";
private static final String WIDGET_NAME_SCREENS_DROPDOWN = "ScreensDropdown"; private static final String WIDGET_NAME_SCREENS_DROPDOWN = "ScreensDropdown";
...@@ -166,6 +167,10 @@ public class DesignToolbar extends Toolbar { ...@@ -166,6 +167,10 @@ public class DesignToolbar extends Toolbar {
// width of palette minus cellspacing/border of buttons // width of palette minus cellspacing/border of buttons
toolbar.setCellWidth(projectNameLabel, "222px"); 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(); List<DropDownItem> screenItems = Lists.newArrayList();
addDropDownButton(WIDGET_NAME_SCREENS_DROPDOWN, MESSAGES.screensButton(), screenItems); addDropDownButton(WIDGET_NAME_SCREENS_DROPDOWN, MESSAGES.screensButton(), screenItems);
...@@ -186,6 +191,19 @@ public class DesignToolbar extends Toolbar { ...@@ -186,6 +191,19 @@ public class DesignToolbar extends Toolbar {
Ode.getInstance().getTopToolbar().updateFileMenuButtons(0); 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 { private class AddFormAction implements Command {
@Override @Override
public void execute() { public void execute() {
...@@ -518,4 +536,12 @@ public class DesignToolbar extends Toolbar { ...@@ -518,4 +536,12 @@ public class DesignToolbar extends Toolbar {
return currentView; 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; ...@@ -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.DialogBox;
import com.google.gwt.user.client.ui.DockPanel; import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel; 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.Grid;
import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasHorizontalAlignment; import com.google.gwt.user.client.ui.HasHorizontalAlignment;
...@@ -207,15 +208,17 @@ public class Ode implements EntryPoint { ...@@ -207,15 +208,17 @@ public class Ode implements EntryPoint {
* |+-- topPanel -------------------------------+| * |+-- topPanel -------------------------------+|
* || || * || ||
* |+-------------------------------------------+| * |+-------------------------------------------+|
* |+-- deckPanel ------------------------------+| * |+-- overDeckPanel --+-----------------------+|
* || || * || tutorialPanel | deckPanel ||
* |+-------------------------------------------+| * |+-------------------+-----------------------+|
* |+-- statusPanel ----------------------------+| * |+-- statusPanel ----------------------------+|
* || || * || ||
* |+-------------------------------------------+| * |+-------------------------------------------+|
* +---------------------------------------------+ * +---------------------------------------------+
*/ */
private DeckPanel deckPanel; private DeckPanel deckPanel;
private HorizontalPanel overDeckPanel;
private Frame tutorialPanel;
private int projectsTabIndex; private int projectsTabIndex;
private int designTabIndex; private int designTabIndex;
private int debuggingTabIndex; private int debuggingTabIndex;
...@@ -236,6 +239,9 @@ public class Ode implements EntryPoint { ...@@ -236,6 +239,9 @@ public class Ode implements EntryPoint {
private DesignToolbar designToolbar; private DesignToolbar designToolbar;
private TopToolbar topToolbar; 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 // Popup that indicates that an asynchronous request is pending. It is visible
// initially, and will be hidden automatically after the first RPC completes. // initially, and will be hidden automatically after the first RPC completes.
private static RpcStatusPopup rpcStatusPopup; private static RpcStatusPopup rpcStatusPopup;
...@@ -410,6 +416,7 @@ public class Ode implements EntryPoint { ...@@ -410,6 +416,7 @@ public class Ode implements EntryPoint {
public void switchToProjectsView() { public void switchToProjectsView() {
// We may need to pass the code below as a runnable to // We may need to pass the code below as a runnable to
// screenShotMaybe() so build the runnable now // screenShotMaybe() so build the runnable now
hideTutorials();
Runnable next = new Runnable() { Runnable next = new Runnable() {
@Override @Override
public void run() { public void run() {
...@@ -440,6 +447,7 @@ public class Ode implements EntryPoint { ...@@ -440,6 +447,7 @@ public class Ode implements EntryPoint {
*/ */
public void switchToUserAdminPanel() { public void switchToUserAdminPanel() {
hideTutorials();
currentView = USERADMIN; currentView = USERADMIN;
deckPanel.showWidget(userAdminTabIndex); deckPanel.showWidget(userAdminTabIndex);
} }
...@@ -448,6 +456,7 @@ public class Ode implements EntryPoint { ...@@ -448,6 +456,7 @@ public class Ode implements EntryPoint {
* Switch to the Gallery tab * Switch to the Gallery tab
*/ */
public void switchToGalleryView() { public void switchToGalleryView() {
hideTutorials();
if (!galleryInitialized) { if (!galleryInitialized) {
// Gallery initialization is deferred until now. // Gallery initialization is deferred until now.
initializeGallery(); initializeGallery();
...@@ -460,6 +469,7 @@ public class Ode implements EntryPoint { ...@@ -460,6 +469,7 @@ public class Ode implements EntryPoint {
* Switch to the Gallery App * Switch to the Gallery App
*/ */
public void switchToGalleryAppView(GalleryApp app, int editStatus) { public void switchToGalleryAppView(GalleryApp app, int editStatus) {
hideTutorials();
if (!galleryInitialized) { if (!galleryInitialized) {
// Gallery initialization is deferred until now. // Gallery initialization is deferred until now.
initializeGallery(); initializeGallery();
...@@ -474,6 +484,7 @@ public class Ode implements EntryPoint { ...@@ -474,6 +484,7 @@ public class Ode implements EntryPoint {
* TODO: change string parameter * TODO: change string parameter
*/ */
public void switchToUserProfileView(String userId, int editStatus) { public void switchToUserProfileView(String userId, int editStatus) {
hideTutorials();
currentView = USERPROFILE; currentView = USERPROFILE;
OdeLog.log("###########" + userId + "||||||" + editStatus); OdeLog.log("###########" + userId + "||||||" + editStatus);
ProfileBox.setProfile(userId, editStatus); ProfileBox.setProfile(userId, editStatus);
...@@ -486,6 +497,7 @@ public class Ode implements EntryPoint { ...@@ -486,6 +497,7 @@ public class Ode implements EntryPoint {
public void switchToDesignView() { public void switchToDesignView() {
// Only show designer if there is a current editor. // Only show designer if there is a current editor.
// ***** THE DESIGNER TAB DOES NOT DISPLAY CORRECTLY IF THERE IS NO CURRENT EDITOR. ***** // ***** THE DESIGNER TAB DOES NOT DISPLAY CORRECTLY IF THERE IS NO CURRENT EDITOR. *****
showTutorials();
currentView = DESIGNER; currentView = DESIGNER;
getTopToolbar().updateFileMenuButtons(currentView); getTopToolbar().updateFileMenuButtons(currentView);
if (currentFileEditor != null) { if (currentFileEditor != null) {
...@@ -508,6 +520,7 @@ public class Ode implements EntryPoint { ...@@ -508,6 +520,7 @@ public class Ode implements EntryPoint {
* Switch to the Moderation Page tab * Switch to the Moderation Page tab
*/ */
public void switchToModerationPageView() { public void switchToModerationPageView() {
hideTutorials();
if (!galleryInitialized) { if (!galleryInitialized) {
initializeGallery(); initializeGallery();
} }
...@@ -518,6 +531,7 @@ public class Ode implements EntryPoint { ...@@ -518,6 +531,7 @@ public class Ode implements EntryPoint {
* Switch to the Debugging tab * Switch to the Debugging tab
*/ */
public void switchToDebuggingView() { public void switchToDebuggingView() {
hideTutorials();
deckPanel.showWidget(debuggingTabIndex); deckPanel.showWidget(debuggingTabIndex);
// NOTE(lizlooney) - Calling resizeWorkArea for debuggingTab prevents the // NOTE(lizlooney) - Calling resizeWorkArea for debuggingTab prevents the
...@@ -908,6 +922,15 @@ public class Ode implements EntryPoint { ...@@ -908,6 +922,15 @@ public class Ode implements EntryPoint {
DockPanel mainPanel = new DockPanel(); DockPanel mainPanel = new DockPanel();
mainPanel.add(topPanel, DockPanel.NORTH); 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 // Create tab panel for subsequent tabs
deckPanel = new DeckPanel() { deckPanel = new DeckPanel() {
@Override @Override
...@@ -1122,9 +1145,15 @@ public class Ode implements EntryPoint { ...@@ -1122,9 +1145,15 @@ public class Ode implements EntryPoint {
// ***** THE DESIGNER TAB DOES NOT DISPLAY CORRECTLY IF THERE IS NO CURRENT PROJECT. ***** // ***** THE DESIGNER TAB DOES NOT DISPLAY CORRECTLY IF THERE IS NO CURRENT PROJECT. *****
deckPanel.showWidget(projectsTabIndex); deckPanel.showWidget(projectsTabIndex);
mainPanel.add(deckPanel, DockPanel.CENTER); overDeckPanel = new HorizontalPanel();
mainPanel.setCellHeight(deckPanel, "100%"); overDeckPanel.setHeight("100%");
mainPanel.setCellWidth(deckPanel, "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(switchToDesignerButton, DockPanel.WEST);
// mainPanel.add(switchToBlocksButton, DockPanel.EAST); // mainPanel.add(switchToBlocksButton, DockPanel.EAST);
...@@ -2322,6 +2351,59 @@ public class Ode implements EntryPoint { ...@@ -2322,6 +2351,59 @@ public class Ode implements EntryPoint {
return container; 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 // Native code to set the top level rendezvousServer variable
// where blockly code can easily find it. // where blockly code can easily find it.
private native void setRendezvousServer(String server) /*-{ private native void setRendezvousServer(String server) /*-{
......
...@@ -102,6 +102,10 @@ public interface OdeMessages extends Messages { ...@@ -102,6 +102,10 @@ public interface OdeMessages extends Messages {
@Description("Label of the button for checkpoint") @Description("Label of the button for checkpoint")
String checkpointButton(); String checkpointButton();
@DefaultMessage("Toggle Tutorial")
@Description("Label for the Toggle Tutorial Button")
String toggleTutorialButton();
@DefaultMessage("Add Screen ...") @DefaultMessage("Add Screen ...")
@Description("Label of the button for adding a new screen") @Description("Label of the button for adding a new screen")
String addFormButton(); String addFormButton();
...@@ -3036,6 +3040,10 @@ public interface OdeMessages extends Messages { ...@@ -3036,6 +3040,10 @@ public interface OdeMessages extends Messages {
@Description("") @Description("")
String VersionNameProperties(); String VersionNameProperties();
@DefaultMessage("TutorialURL")
@Description("")
String TutorialURLProperties();
@DefaultMessage("Sizing") @DefaultMessage("Sizing")
@Description("") @Description("")
String SizingProperties(); String SizingProperties();
......
...@@ -235,8 +235,15 @@ public abstract class ProjectEditor extends Composite { ...@@ -235,8 +235,15 @@ public abstract class ProjectEditor extends Composite {
Settings settings = projectSettings.getSettings(category); Settings settings = projectSettings.getSettings(category);
String currentValue = settings.getPropertyValue(name); String currentValue = settings.getPropertyValue(name);
if (!newValue.equals(currentValue)) { if (!newValue.equals(currentValue)) {
OdeLog.log("ProjectEditor: changeProjectSettingsProperty: " + name + " " + currentValue +
" => " + newValue);
settings.changePropertyValue(name, 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 { ...@@ -308,6 +315,12 @@ public abstract class ProjectEditor extends Composite {
// project just after the editor is created. // project just after the editor is created.
OdeLog.log("ProjectEditor: got onLoad for project " + projectId); OdeLog.log("ProjectEditor: got onLoad for project " + projectId);
super.onLoad(); 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(); onShow();
} }
...@@ -315,6 +328,9 @@ public abstract class ProjectEditor extends Composite { ...@@ -315,6 +328,9 @@ public abstract class ProjectEditor extends Composite {
@Override @Override
protected void onUnload() { protected void onUnload() {
// onUnload is called immediately before a widget becomes detached from the browser's document. // 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); OdeLog.log("ProjectEditor: got onUnload for project " + projectId);
super.onUnload(); super.onUnload();
onHide(); onHide();
......
...@@ -177,6 +177,7 @@ public final class MockForm extends MockContainer { ...@@ -177,6 +177,7 @@ public final class MockForm extends MockContainer {
private static final String PROPERTY_NAME_SIZING = "Sizing"; // Don't show except on screen1 private static final String PROPERTY_NAME_SIZING = "Sizing"; // Don't show except on screen1
// 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_SHOW_LISTS_AS_JSON = "ShowListsAsJson";
private static final String PROPERTY_NAME_TUTORIAL_URL = "TutorialURL";
// Form UI components // Form UI components
AbsolutePanel formWidget; AbsolutePanel formWidget;
...@@ -429,6 +430,11 @@ public final class MockForm extends MockContainer { ...@@ -429,6 +430,11 @@ public final class MockForm extends MockContainer {
return editor.isScreen1(); 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); return super.isPropertyVisible(propertyName);
} }
...@@ -534,7 +540,7 @@ public final class MockForm extends MockContainer { ...@@ -534,7 +540,7 @@ public final class MockForm extends MockContainer {
private void setShowListsAsJsonProperty(String asJson) { private void setShowListsAsJsonProperty(String asJson) {
// This property actually applies to the application and is only visible on // 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 // 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()) { if (editor.isScreen1()) {
editor.getProjectEditor().changeProjectSettingsProperty( editor.getProjectEditor().changeProjectSettingsProperty(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
...@@ -542,6 +548,17 @@ public final class MockForm extends MockContainer { ...@@ -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) { private void setANameProperty(String aname) {
// The AppName property actually applies to the application and is only visible on Screen1. // 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 // 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 { ...@@ -821,6 +838,8 @@ public final class MockForm extends MockContainer {
setANameProperty(newValue); setANameProperty(newValue);
} else if (propertyName.equals(PROPERTY_NAME_SHOW_LISTS_AS_JSON)) { } else if (propertyName.equals(PROPERTY_NAME_SHOW_LISTS_AS_JSON)) {
setShowListsAsJsonProperty(newValue); setShowListsAsJsonProperty(newValue);
} else if (propertyName.equals(PROPERTY_NAME_TUTORIAL_URL)) {
setTutorialURLProperty(newValue);
} else if (propertyName.equals(PROPERTY_NAME_HORIZONTAL_ALIGNMENT)) { } else if (propertyName.equals(PROPERTY_NAME_HORIZONTAL_ALIGNMENT)) {
myLayout.setHAlignmentFlags(newValue); myLayout.setHAlignmentFlags(newValue);
refreshForm(); refreshForm();
...@@ -850,20 +869,24 @@ public final class MockForm extends MockContainer { ...@@ -850,20 +869,24 @@ public final class MockForm extends MockContainer {
@Override @Override
public EditableProperties getProperties() { public EditableProperties getProperties() {
// Before we return the Properties object, we make sure that the // Before we return the Properties object, we make sure that the
// Sizing and ShowListsAsJson properties have the value from the // Sizing, ShowListsAsJson and TutorialURL properties have the
// project's properties this is because these are per project, not // value from the project's properties this is because these are
// per Screen(Form) We only have to do this on screens other then // per project, not per Screen(Form) We only have to do this on
// screen1 because screen1's value is definitive. // screens other then screen1 because screen1's value is
// definitive.
if (!editor.isScreen1()) { if (!editor.isScreen1()) {
properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING, properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING,
editor.getProjectEditor().getProjectSettingsProperty( editor.getProjectEditor().getProjectSettingsProperty(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING)); SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING));
// new code to test
properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON, properties.changePropertyValue(SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON,
editor.getProjectEditor().getProjectSettingsProperty( editor.getProjectEditor().getProjectSettingsProperty(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON)); 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; return properties;
} }
......
...@@ -51,6 +51,9 @@ public final class YoungAndroidSettings extends Settings { ...@@ -51,6 +51,9 @@ public final class YoungAndroidSettings extends Settings {
addProperty(new EditableProperty(this, addProperty(new EditableProperty(this,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON, "false", SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON, "false",
EditableProperty.TYPE_INVISIBLE)); 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 { ...@@ -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 // so that it contains the correct entries for "main" and "name", which are dependent on
// the projectName and qualifiedFormName. // the projectName and qualifiedFormName.
String content = YoungAndroidProjectService.getProjectPropertiesFileContents( 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)); project.addTextFile(new TextFile(fileName, content));
isProjectArchive = true; isProjectArchive = true;
...@@ -149,7 +149,7 @@ public final class FileImporterImpl implements FileImporter { ...@@ -149,7 +149,7 @@ public final class FileImporterImpl implements FileImporter {
if (projectHistory != null) { if (projectHistory != null) {
project.setProjectHistory(projectHistory); 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); long projectId = storageIo.createProject(userId, project, settings);
return storageIo.getUserProject(userId, projectId); return storageIo.getUserProject(userId, projectId);
} }
......
...@@ -129,7 +129,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -129,7 +129,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
* Returns project settings that can be used when creating a new project. * Returns project settings that can be used when creating a new project.
*/ */
public static String getProjectSettings(String icon, String vCode, String vName, 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); icon = Strings.nullToEmpty(icon);
vCode = Strings.nullToEmpty(vCode); vCode = Strings.nullToEmpty(vCode);
vName = Strings.nullToEmpty(vName); vName = Strings.nullToEmpty(vName);
...@@ -137,6 +137,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -137,6 +137,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
sizing = Strings.nullToEmpty(sizing); sizing = Strings.nullToEmpty(sizing);
aName = Strings.nullToEmpty(aName); aName = Strings.nullToEmpty(aName);
showListsAsJson = Strings.nullToEmpty(showListsAsJson); showListsAsJson = Strings.nullToEmpty(showListsAsJson);
tutorialURL = Strings.nullToEmpty(tutorialURL);
return "{\"" + SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS + "\":{" + return "{\"" + SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS + "\":{" +
"\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_ICON + "\":\"" + icon + "\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_ICON + "\":\"" + icon +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_VERSION_CODE + "\":\"" + vCode + "\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_VERSION_CODE + "\":\"" + vCode +
...@@ -145,6 +146,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -145,6 +146,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME + "\":\"" + aName + "\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME + "\":\"" + aName +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING + "\":\"" + sizing + "\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING + "\":\"" + sizing +
"\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON + "\":\"" + showListsAsJson + "\",\"" + 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 { ...@@ -159,7 +161,8 @@ public final class YoungAndroidProjectService extends CommonProjectService {
* @param vname the version name * @param vname the version name
*/ */
public static String getProjectPropertiesFileContents(String projectName, String qualifiedName, 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" + String contents = "main=" + qualifiedName + "\n" +
"name=" + projectName + '\n' + "name=" + projectName + '\n' +
"assets=../" + ASSETS_FOLDER + "\n" + "assets=../" + ASSETS_FOLDER + "\n" +
...@@ -186,6 +189,9 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -186,6 +189,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
if (showListsAsJson != null && !showListsAsJson.isEmpty()) { if (showListsAsJson != null && !showListsAsJson.isEmpty()) {
contents += "showlistsasjson=" + showListsAsJson + "\n"; contents += "showlistsasjson=" + showListsAsJson + "\n";
} }
if (tutorialURL != null && !tutorialURL.isEmpty()) {
contents += "tutorialurl=" + tutorialURL + "\n";
}
return contents; return contents;
} }
...@@ -255,6 +261,9 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -255,6 +261,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String newShowListsAsJson = Strings.nullToEmpty(settings.getSetting( String newShowListsAsJson = Strings.nullToEmpty(settings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON)); 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( String newAName = Strings.nullToEmpty(settings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME)); SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME));
...@@ -277,16 +286,18 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -277,16 +286,18 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String oldSizing = Strings.nullToEmpty(properties.getProperty("sizing")); String oldSizing = Strings.nullToEmpty(properties.getProperty("sizing"));
String oldAName = Strings.nullToEmpty(properties.getProperty("aname")); String oldAName = Strings.nullToEmpty(properties.getProperty("aname"));
String oldShowListsAsJson = Strings.nullToEmpty(properties.getProperty("showlistsasjson")); String oldShowListsAsJson = Strings.nullToEmpty(properties.getProperty("showlistsasjson"));
String oldTutorialURL = Strings.nullToEmpty(properties.getProperty("tutorialurl"));
if (!newIcon.equals(oldIcon) || !newVCode.equals(oldVCode) || !newVName.equals(oldVName) if (!newIcon.equals(oldIcon) || !newVCode.equals(oldVCode) || !newVName.equals(oldVName)
|| !newUsesLocation.equals(oldUsesLocation) || || !newUsesLocation.equals(oldUsesLocation) ||
!newAName.equals(oldAName) || !newSizing.equals(oldSizing) || !newAName.equals(oldAName) || !newSizing.equals(oldSizing) ||
!newShowListsAsJson.equals(oldShowListsAsJson)) { !newShowListsAsJson.equals(oldShowListsAsJson) ||
!newTutorialURL.equals(oldTutorialURL)) {
// Recreate the project.properties and upload it to storageIo. // Recreate the project.properties and upload it to storageIo.
String projectName = properties.getProperty("name"); String projectName = properties.getProperty("name");
String qualifiedName = properties.getProperty("main"); String qualifiedName = properties.getProperty("main");
String newContent = getProjectPropertiesFileContents(projectName, qualifiedName, newIcon, 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, storageIo.uploadFileForce(projectId, PROJECT_PROPERTIES_FILE_NAME, userId,
newContent, StorageUtil.DEFAULT_CHARSET); newContent, StorageUtil.DEFAULT_CHARSET);
} }
...@@ -305,7 +316,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -305,7 +316,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String propertiesFileName = PROJECT_PROPERTIES_FILE_NAME; String propertiesFileName = PROJECT_PROPERTIES_FILE_NAME;
String propertiesFileContents = getProjectPropertiesFileContents(projectName, 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 formFileName = YoungAndroidFormNode.getFormFileId(qualifiedFormName);
String formFileContents = getInitialFormPropertiesFileContents(qualifiedFormName); String formFileContents = getInitialFormPropertiesFileContents(qualifiedFormName);
...@@ -325,7 +336,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -325,7 +336,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
project.addTextFile(new TextFile(yailFileName, yailFileContents)); project.addTextFile(new TextFile(yailFileName, yailFileContents));
// Create new project // 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 @Override
...@@ -355,6 +366,9 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -355,6 +366,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String showListsAsJson = oldSettings.getSetting( String showListsAsJson = oldSettings.getSetting(
SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS,
SettingsConstants.YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON); 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); Project newProject = new Project(newName);
newProject.setProjectType(YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE); newProject.setProjectType(YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE);
...@@ -374,7 +388,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -374,7 +388,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
String qualifiedFormName = StringUtils.getQualifiedFormName( String qualifiedFormName = StringUtils.getQualifiedFormName(
storageIo.getUser(userId).getUserEmail(), newName); storageIo.getUser(userId).getUserEmail(), newName);
newContents = getProjectPropertiesFileContents(newName, qualifiedFormName, icon, vcode, newContents = getProjectPropertiesFileContents(newName, qualifiedFormName, icon, vcode,
vname, useslocation, aname, sizing, showListsAsJson); vname, useslocation, aname, sizing, showListsAsJson, tutorialURL);
} else { } else {
// This is some file other than the project properties file. // This is some file other than the project properties file.
// oldSourceFileName may contain the old project name as a path segment, surrounded by /. // oldSourceFileName may contain the old project name as a path segment, surrounded by /.
...@@ -398,7 +412,7 @@ public final class YoungAndroidProjectService extends CommonProjectService { ...@@ -398,7 +412,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
// Create the new project and return the new project's id. // Create the new project and return the new project's id.
return storageIo.createProject(userId, newProject, getProjectSettings(icon, vcode, vname, return storageIo.createProject(userId, newProject, getProjectSettings(icon, vcode, vname,
useslocation, aname, sizing, showListsAsJson)); useslocation, aname, sizing, showListsAsJson, tutorialURL));
} }
@Override @Override
......
...@@ -50,6 +50,7 @@ public class SettingsConstants { ...@@ -50,6 +50,7 @@ public class SettingsConstants {
public static final String YOUNG_ANDROID_SETTINGS_SIZING = "Sizing"; 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_APP_NAME = "AppName";
public static final String YOUNG_ANDROID_SETTINGS_SHOW_LISTS_AS_JSON = "ShowListsAsJson"; 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. * Settings for the Blocks editor.
......
...@@ -492,7 +492,8 @@ public class ProjectServiceTest { ...@@ -492,7 +492,8 @@ public class ProjectServiceTest {
SettingsConstants.YOUNG_ANDROID_SETTINGS_USES_LOCATION + "\":\"false\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_USES_LOCATION + "\":\"false\",\"" +
SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME + "\":\"Project1\",\"" + SettingsConstants.YOUNG_ANDROID_SETTINGS_APP_NAME + "\":\"Project1\",\"" +
SettingsConstants.YOUNG_ANDROID_SETTINGS_SIZING + "\":\"Fixed\",\"" + 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); loadedSettings);
String storedSettings = String storedSettings =
......
...@@ -371,11 +371,17 @@ Blockly.Yail.getFormPropertiesLines = function(formName, componentJson, includeC ...@@ -371,11 +371,17 @@ Blockly.Yail.getFormPropertiesLines = function(formName, componentJson, includeC
* @param {Blockly.ComponentDatabase} componentDb The workspace's database of components and types. * @param {Blockly.ComponentDatabase} componentDb The workspace's database of components and types.
* @returns {Array} code strings * @returns {Array} code strings
* @private * @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) { Blockly.Yail.getPropertySettersLines = function(componentJson, componentName, componentDb) {
var code = []; var code = [];
for (var prop in componentJson) { 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, code.push(Blockly.Yail.getPropertySetterString(componentName, componentJson.$Type, prop,
componentJson[prop], componentDb)); componentJson[prop], componentDb));
} }
......
...@@ -1488,6 +1488,16 @@ public class Form extends Activity ...@@ -1488,6 +1488,16 @@ public class Form extends Activity
return formHeight; 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. * Display a new form.
* *
......
...@@ -988,6 +988,15 @@ none ...@@ -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> <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> <dt><code>Title</code></dt>
<dd>The caption for the form, which apears in the title bar</dd> <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> <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> <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> <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