Java tutorial
// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client; import java.util.Random; import static com.google.appinventor.client.Ode.MESSAGES; import java.util.List; import java.util.logging.Logger; import com.google.appinventor.client.boxes.AdminUserListBox; import com.google.appinventor.client.boxes.AssetListBox; import com.google.appinventor.client.boxes.BlockSelectorBox; import com.google.appinventor.client.boxes.PrivateUserProfileTabPanel; import com.google.appinventor.client.boxes.MessagesOutputBox; import com.google.appinventor.client.boxes.OdeLogBox; import com.google.appinventor.client.boxes.PaletteBox; import com.google.appinventor.client.boxes.ProjectListBox; import com.google.appinventor.client.boxes.ModerationPageBox; import com.google.appinventor.client.boxes.GalleryListBox; import com.google.appinventor.client.boxes.GalleryAppBox; import com.google.appinventor.client.boxes.ProfileBox; import com.google.appinventor.client.boxes.PropertiesBox; import com.google.appinventor.client.boxes.SourceStructureBox; import com.google.appinventor.client.boxes.ViewerBox; import com.google.appinventor.client.editor.EditorManager; import com.google.appinventor.client.editor.FileEditor; import com.google.appinventor.client.editor.youngandroid.BlocklyPanel; import com.google.appinventor.client.explorer.commands.ChainableCommand; import com.google.appinventor.client.explorer.commands.CommandRegistry; import com.google.appinventor.client.explorer.commands.SaveAllEditorsCommand; import com.google.appinventor.client.explorer.project.Project; import com.google.appinventor.client.explorer.project.ProjectChangeAdapter; import com.google.appinventor.client.explorer.project.ProjectManager; import com.google.appinventor.client.explorer.project.ProjectManagerEventAdapter; import com.google.appinventor.client.explorer.youngandroid.GalleryPage; import com.google.appinventor.client.explorer.youngandroid.GalleryToolbar; import com.google.appinventor.client.explorer.youngandroid.ProjectToolbar; import com.google.appinventor.client.jsonp.JsonpConnection; import com.google.appinventor.client.output.OdeLog; import com.google.appinventor.client.settings.Settings; import com.google.appinventor.client.settings.user.UserSettings; import com.google.appinventor.client.tracking.Tracking; import com.google.appinventor.client.utils.PZAwarePositionCallback; import com.google.appinventor.client.widgets.boxes.Box; import com.google.appinventor.client.widgets.boxes.ColumnLayout; import com.google.appinventor.client.widgets.boxes.ColumnLayout.Column; import com.google.appinventor.client.widgets.boxes.WorkAreaPanel; import com.google.appinventor.client.wizards.NewProjectWizard.NewProjectCommand; import com.google.appinventor.client.wizards.TemplateUploadWizard; import com.google.appinventor.common.version.AppInventorFeatures; import com.google.appinventor.components.common.YaVersion; import com.google.appinventor.shared.rpc.component.ComponentService; import com.google.appinventor.shared.rpc.component.ComponentServiceAsync; import com.google.appinventor.shared.rpc.GetMotdService; import com.google.appinventor.shared.rpc.GetMotdServiceAsync; import com.google.appinventor.shared.rpc.ServerLayout; import com.google.appinventor.shared.rpc.admin.AdminInfoService; import com.google.appinventor.shared.rpc.admin.AdminInfoServiceAsync; import com.google.appinventor.shared.rpc.help.HelpService; import com.google.appinventor.shared.rpc.help.HelpServiceAsync; import com.google.appinventor.shared.rpc.launch.LaunchService; import com.google.appinventor.shared.rpc.launch.LaunchServiceAsync; import com.google.appinventor.shared.rpc.project.FileNode; import com.google.appinventor.shared.rpc.project.GalleryAppListResult; import com.google.appinventor.shared.rpc.project.GallerySettings; import com.google.appinventor.shared.rpc.project.ProjectRootNode; import com.google.appinventor.shared.rpc.project.ProjectService; import com.google.appinventor.shared.rpc.project.ProjectServiceAsync; import com.google.appinventor.shared.rpc.project.GalleryService; import com.google.appinventor.shared.rpc.project.GalleryServiceAsync; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidSourceNode; import com.google.appinventor.shared.rpc.user.Config; import com.google.appinventor.shared.rpc.user.SplashConfig; import com.google.appinventor.shared.rpc.user.User; import com.google.appinventor.shared.rpc.user.UserInfoService; import com.google.appinventor.shared.rpc.user.UserInfoServiceAsync; import com.google.appinventor.shared.settings.SettingsConstants; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.MouseWheelEvent; import com.google.gwt.event.dom.client.MouseWheelHandler; import com.google.gwt.event.logical.shared.ResizeEvent; import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.http.client.Response; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.History; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.StatusCodeException; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.ClickListener; 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.Grid; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HasHorizontalAlignment; import com.google.gwt.user.client.ui.HasVerticalAlignment; 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.PushButton; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.google.appinventor.shared.rpc.project.GalleryApp; /** * Main entry point for Ode. Defines the startup UI elements in * {@link #onModuleLoad()}. * */ public class Ode implements EntryPoint { private static final Logger LOG = Logger.getLogger(Ode.class.getName()); // I18n messages public static final OdeMessages MESSAGES = GWT.create(OdeMessages.class); // Global instance of the Ode object private static Ode instance; // Application level image bundle private static final Images IMAGES = GWT.create(Images.class); // ProjectEditor registry private static final ProjectEditorRegistry EDITORS = new ProjectEditorRegistry(); // Command registry private static final CommandRegistry COMMANDS = new CommandRegistry(); // System config private static Config config; // User settings private static UserSettings userSettings; // Gallery settings private static GallerySettings gallerySettings; private MotdFetcher motdFetcher; // User information private User user; // Template path if set by /?repo= private String templatePath; private boolean templateLoadingFlag = false; // Gallery id if set by /?galleryId= private String galleryId; private boolean galleryIdLoadingFlag = false; // Nonce Information private String nonce; // Read Only Flag: If true, the UI will not permit operations which permit // write requests private boolean isReadOnly; private String sessionId = generateUuid(); // Create new session id private Random random = new Random(); // For generating random nonce // Collection of projects private ProjectManager projectManager; // Collection of editors private EditorManager editorManager; // Currently active file editor, could be a YaFormEditor or a YaBlocksEditor or null. private FileEditor currentFileEditor; private AssetManager assetManager; // Remembers the current View static final int DESIGNER = 0; static final int PROJECTS = 1; private static final int GALLERY = 2; private static final int GALLERYAPP = 3; private static final int USERPROFILE = 4; private static final int PRIVATEUSERPROFILE = 5; private static final int MODERATIONPAGE = 6; private static final int USERADMIN = 7; private static int currentView = DESIGNER; /* * The following fields define the general layout of the UI as seen in the following diagram: * * +-- mainPanel --------------------------------+ * |+-- topPanel -------------------------------+| * || || * |+-------------------------------------------+| * |+-- deckPanel ------------------------------+| * || || * |+-------------------------------------------+| * |+-- statusPanel ----------------------------+| * || || * |+-------------------------------------------+| * +---------------------------------------------+ */ private DeckPanel deckPanel; private int projectsTabIndex; private int designTabIndex; private int debuggingTabIndex; private int galleryTabIndex; private int galleryAppTabIndex; private int userAdminTabIndex; private int userProfileTabIndex; private int privateUserProfileIndex; private int moderationPageTabIndex; private TopPanel topPanel; private StatusPanel statusPanel; private HorizontalPanel workColumns; private VerticalPanel structureAndAssets; private ProjectToolbar projectToolbar; private GalleryToolbar galleryListToolbar; private GalleryToolbar galleryPageToolbar; private AdminUserListBox uaListBox; private DesignToolbar designToolbar; private TopToolbar topToolbar; // 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; // Web service for help information private final HelpServiceAsync helpService = GWT.create(HelpService.class); // Web service for project related information private final ProjectServiceAsync projectService = GWT.create(ProjectService.class); // Web service for gallery related information private final GalleryServiceAsync galleryService = GWT.create(GalleryService.class); // Web service for user related information private final UserInfoServiceAsync userInfoService = GWT.create(UserInfoService.class); // Web service for launch related services private final LaunchServiceAsync launchService = GWT.create(LaunchService.class); // Web service for get motd information private final GetMotdServiceAsync getMotdService = GWT.create(GetMotdService.class); // Web service for component related operations private final ComponentServiceAsync componentService = GWT.create(ComponentService.class); private final AdminInfoServiceAsync adminInfoService = GWT.create(AdminInfoService.class); private boolean windowClosing; private boolean screensLocked; private SplashConfig splashConfig; // Splash Screen Configuration /** * Returns global instance of Ode. * * @return global Ode instance */ public static Ode getInstance() { return instance; } /** * Returns instance of the aggregate image bundle for the application. * * @return image bundle */ public static Images getImageBundle() { return IMAGES; } /** * Returns the editor registry. * * @return the editor registry */ public static ProjectEditorRegistry getProjectEditorRegistry() { return EDITORS; } /** * Returns the command registry. * * @return the command registry */ public static CommandRegistry getCommandRegistry() { return COMMANDS; } /** * Returns the system config. * * @return system config */ public static Config getSystemConfig() { return config; } /** * Returns the user settings. * * @return user settings */ public static UserSettings getUserSettings() { return userSettings; } /** * Returns the gallery settings. * * @return gallery settings */ public static GallerySettings getGallerySettings() { return gallerySettings; } /** * loads the gallery settings from server * */ public void loadGallerySettings() { // Callback for when the server returns us the apps final Ode ode = Ode.getInstance(); final OdeAsyncCallback<GallerySettings> callback = new OdeAsyncCallback<GallerySettings>( // failure message MESSAGES.gallerySettingsError()) { @Override public void onSuccess(GallerySettings settings) { gallerySettings = settings; if (gallerySettings.galleryEnabled() == true) { ProjectListBox.getProjectListBox().getProjectList().setPublishedHeaderVisible(true); projectToolbar.setPublishOrUpdateButtonVisible(true); GalleryClient.getInstance().setSystemEnvironment(settings.getEnvironment()); GalleryListBox.loadGalleryList(); topPanel.showGalleryLink(true); if (user.isModerator()) { ModerationPageBox.loadModerationPage(); topPanel.showModerationLink(true); } topPanel.updateAccountMessageButton(); PrivateUserProfileTabPanel.getPrivateUserProfileTabPanel().loadProfileImage(); } else { topPanel.showModerationLink(false); topPanel.showGalleryLink(false); projectToolbar.setPublishOrUpdateButtonVisible(false); ProjectListBox.getProjectListBox().getProjectList().setPublishedHeaderVisible(false); } } }; //this is below the call back, but of course it is done first ode.getGalleryService().loadGallerySettings(callback); } /** * Returns the asset manager. * * @return asset manager */ public AssetManager getAssetManager() { return assetManager; } /** * Returns true if we have received the window closing event. */ public static boolean isWindowClosing() { return getInstance().windowClosing; } /** * Get the current view */ public int getCurrentView() { return currentView; } /** * Switch to the Projects tab */ public void switchToProjectsView() { if (currentView != PROJECTS) { //If we are switching to projects view from somewhere else, clear all of the previously selected projects. ProjectListBox.getProjectListBox().getProjectList().getSelectedProjects().clear(); ProjectListBox.getProjectListBox().getProjectList().refreshTable(false); } currentView = PROJECTS; getTopToolbar().updateFileMenuButtons(currentView); deckPanel.showWidget(projectsTabIndex); // If we started a project, then the start button was disabled (to avoid // a second press while the new project wizard was starting (aka we "debounce" // the button). When the person switches to the projects list view again (here) // we re-enable it. projectToolbar.enableStartButton(); } /** * Switch to the User Admin Panel */ public void switchToUserAdminPanel() { currentView = USERADMIN; deckPanel.showWidget(userAdminTabIndex); } /** * Switch to the Gallery tab */ public void switchToGalleryView() { currentView = GALLERY; deckPanel.showWidget(galleryTabIndex); } /** * Switch to the Gallery App */ public void switchToGalleryAppView(GalleryApp app, int editStatus) { currentView = GALLERYAPP; GalleryAppBox.setApp(app, editStatus); deckPanel.showWidget(galleryAppTabIndex); } /** * Switch to the user profile * TODO: change string parameter */ public void switchToUserProfileView(String userId, int editStatus) { currentView = USERPROFILE; OdeLog.log("###########" + userId + "||||||" + editStatus); ProfileBox.setProfile(userId, editStatus); deckPanel.showWidget(userProfileTabIndex); } /** * Switch to the Designer tab. Shows an error message if there is no currentFileEditor. */ 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. ***** currentView = DESIGNER; getTopToolbar().updateFileMenuButtons(currentView); if (currentFileEditor != null) { deckPanel.showWidget(designTabIndex); } else { OdeLog.wlog("No current file editor to show in designer"); ErrorReporter.reportInfo(MESSAGES.chooseProject()); } } /** * Switch to Gallery TabPanel */ public void switchToPrivateUserProfileView() { currentView = privateUserProfileIndex; deckPanel.showWidget(privateUserProfileIndex); } /** * Switch to the Moderation Page tab */ public void switchToModerationPageView() { currentView = MODERATIONPAGE; deckPanel.showWidget(moderationPageTabIndex); } /** * Switch to the Debugging tab */ public void switchToDebuggingView() { deckPanel.showWidget(debuggingTabIndex); // NOTE(lizlooney) - Calling resizeWorkArea for debuggingTab prevents the // boxes from overlapping each other. resizeWorkArea((WorkAreaPanel) deckPanel.getWidget(debuggingTabIndex)); } public void openPreviousProject() { if (userSettings == null) { OdeLog.wlog("Ignoring openPreviousProject() since userSettings is null"); return; } OdeLog.log("Ode.openPreviousProject called"); final String value = userSettings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS) .getPropertyValue(SettingsConstants.GENERAL_SETTINGS_CURRENT_PROJECT_ID); // Retrieve the userTemplates String userTemplates = userSettings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS) .getPropertyValue(SettingsConstants.USER_TEMPLATE_URLS); TemplateUploadWizard.setStoredTemplateUrls(userTemplates); if (templateLoadingFlag) { // We are loading a template, open it instead // of the last project NewProjectCommand callbackCommand = new NewProjectCommand() { @Override public void execute(Project project) { templateLoadingFlag = false; Ode.getInstance().openYoungAndroidProjectInDesigner(project); } }; TemplateUploadWizard.openProjectFromTemplate(templatePath, callbackCommand); } else if (galleryIdLoadingFlag) { try { long galleryId_Long = Long.valueOf(galleryId); final OdeAsyncCallback<GalleryApp> callback = new OdeAsyncCallback<GalleryApp>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(GalleryApp app) { if (app == null) { openProject(value); Window.alert(MESSAGES.galleryIdNotExist()); } else { Ode.getInstance().switchToGalleryAppView(app, GalleryPage.VIEWAPP); } } }; Ode.getInstance().getGalleryService().getApp(galleryId_Long, callback); } catch (NumberFormatException e) { openProject(value); Window.alert(MESSAGES.galleryIdNotExist()); } } else { openProject(value); } } private void openProject(String projectIdString) { OdeLog.log("Ode.openProject called for " + projectIdString); if (projectIdString.equals("")) { openPreviousProject(); } else if (!projectIdString.equals("0")) { final long projectId = Long.parseLong(projectIdString); Project project = projectManager.getProject(projectId); if (project != null) { openYoungAndroidProjectInDesigner(project); } else { // The project hasn't been added to the ProjectManager yet. // Add a ProjectManagerEventListener so we'll be notified when it has been added. // Alternatively, it is an invalid projectId. In which case, // nothing happens since if the listener eventually fires // it will not match the projectId. projectManager.addProjectManagerEventListener(new ProjectManagerEventAdapter() { @Override public void onProjectAdded(Project project) { if (project.getProjectId() == projectId) { projectManager.removeProjectManagerEventListener(this); openYoungAndroidProjectInDesigner(project); } } }); } } // else projectIdString == 0; do nothing } public void openYoungAndroidProjectInDesigner(final Project project) { ProjectRootNode projectRootNode = project.getRootNode(); if (projectRootNode == null) { // The project nodes haven't been loaded yet. // Add a ProjectChangeListener so we'll be notified when they have been loaded. project.addProjectChangeListener(new ProjectChangeAdapter() { @Override public void onProjectLoaded(Project projectLoaded) { project.removeProjectChangeListener(this); openYoungAndroidProjectInDesigner(project); } }); project.loadProjectNodes(); } else { // The project nodes have been loaded. Tell the viewer to open // the project. This will cause the projects source files to be fetched // asynchronously, and loaded into file editors. ViewerBox.getViewerBox().show(projectRootNode); // Note: we can't call switchToDesignView until the Screen1 file editor // finishes loading. We leave that to setCurrentFileEditor(), which // will get called at the appropriate time. String projectIdString = Long.toString(project.getProjectId()); if (!History.getToken().equals(projectIdString)) { // insert token into history but do not trigger listener event History.newItem(projectIdString, false); } if (assetManager == null) { assetManager = AssetManager.getInstance(); } assetManager.loadAssets(project.getProjectId()); } getTopToolbar().updateFileMenuButtons(1); } /** * Returns i18n compatible messages * @return messages */ public static OdeMessages getMessages() { return MESSAGES; } /** * Returns the rpcStatusPopup object. * @return RpcStatusPopup */ public static RpcStatusPopup getRpcStatusPopup() { return rpcStatusPopup; } /** * Main entry point for Ode. Setting up the UI and the web service * connections. */ @Override public void onModuleLoad() { Tracking.trackPageview(); // Handler for any otherwise unhandled exceptions GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() { @Override public void onUncaughtException(Throwable e) { OdeLog.xlog(e); if (AppInventorFeatures.sendBugReports()) { if (Window.confirm(MESSAGES.internalErrorReportBug())) { Window.open(BugReport.getBugReportLink(e), "_blank", ""); } } else { // Display a confirm dialog with error msg and if 'ok' open the debugging view if (Window.confirm(MESSAGES.internalErrorClickOkDebuggingView())) { Ode.getInstance().switchToDebuggingView(); } } } }); // Define bridge methods to Javascript JsonpConnection.defineBridgeMethod(); // Initialize global Ode instance instance = this; // Let's see if we were started with a repo= parameter which points to a template templatePath = Window.Location.getParameter("repo"); if (templatePath != null) { OdeLog.wlog("Got a template path of " + templatePath); templateLoadingFlag = true; } // Let's see if we were started with a galleryId= parameter which points to a template galleryId = Window.Location.getParameter("galleryId"); if (galleryId != null) { OdeLog.wlog("Got a galleryId of " + galleryId); galleryIdLoadingFlag = true; } // Get user information. OdeAsyncCallback<Config> callback = new OdeAsyncCallback<Config>( // failure message MESSAGES.serverUnavailable()) { @Override public void onSuccess(Config result) { config = result; user = result.getUser(); isReadOnly = user.isReadOnly(); // If user hasn't accepted terms of service, ask them to. if (!user.getUserTosAccepted() && !isReadOnly) { // We expect that the redirect to the TOS page should be handled // by the onFailure method below. The server should return a // "forbidden" error if the TOS wasn't accepted. ErrorReporter.reportError(MESSAGES.serverUnavailable()); return; } splashConfig = result.getSplashConfig(); if (result.getRendezvousServer() != null) { setRendezvousServer(result.getRendezvousServer()); } else { setRendezvousServer(YaVersion.RENDEZVOUS_SERVER); } userSettings = new UserSettings(user); // Gallery settings gallerySettings = new GallerySettings(); //gallerySettings.loadGallerySettings(); loadGallerySettings(); // Initialize project and editor managers // The project manager loads the user's projects asynchronously projectManager = new ProjectManager(); projectManager.addProjectManagerEventListener(new ProjectManagerEventAdapter() { @Override public void onProjectsLoaded() { projectManager.removeProjectManagerEventListener(this); // This handles any built-in templates stored in /war // Retrieve template data stored in war/templates folder and // and save it for later use in TemplateUploadWizard OdeAsyncCallback<String> templateCallback = new OdeAsyncCallback<String>( // failure message MESSAGES.createProjectError()) { @Override public void onSuccess(String json) { // Save the templateData TemplateUploadWizard.initializeBuiltInTemplates(json); // Here we call userSettings.loadSettings, but the settings are actually loaded // asynchronously, so this loadSettings call will return before they are loaded. // After the user settings have been loaded, openPreviousProject will be called. // We have to call this after the builtin templates have been loaded otherwise // we will get a NPF. userSettings.loadSettings(); } }; Ode.getInstance().getProjectService().retrieveTemplateData( TemplateUploadWizard.TEMPLATES_ROOT_DIRECTORY, templateCallback); } }); editorManager = new EditorManager(); // Initialize UI initializeUi(); topPanel.showUserEmail(user.getUserEmail()); } @Override public void onFailure(Throwable caught) { if (caught instanceof StatusCodeException) { StatusCodeException e = (StatusCodeException) caught; int statusCode = e.getStatusCode(); switch (statusCode) { case Response.SC_UNAUTHORIZED: // unauthorized => not on whitelist // getEncodedResponse() gives us the message that we wrote in // OdeAuthFilter.writeWhitelistErrorMessage(). Window.alert(e.getEncodedResponse()); return; case Response.SC_FORBIDDEN: // forbidden => need tos accept Window.open("/" + ServerLayout.YA_TOS_FORM, "_self", null); return; case Response.SC_PRECONDITION_FAILED: String locale = Window.Location.getParameter("locale"); if (locale == null || locale.equals("")) { Window.Location.replace("/login/"); } else { Window.Location.replace("/login/?locale=" + locale); } return; // likely not reached } } super.onFailure(caught); } }; // The call below begins an asynchronous read of the user's settings // When the settings are finished reading, various settings parsers // will be called on the returned JSON object. They will call various // other functions in this module, including openPreviousProject (the // previous project ID is stored in the settings) as well as the splash // screen displaying functions below. // // TODO(user): ODE makes too many RPC requests at startup time. Currently // we do 3 RPCs + 1 per project + 1 per open file. We should bundle some of // those with each other or with the initial HTML transfer. // // This call also stores our sessionId in the backend. This will be checked // when we go to save a file and if different file saving will be disabled // Newer sessions invalidate older sessions. userInfoService.getSystemConfig(sessionId, callback); History.addValueChangeHandler(new ValueChangeHandler<String>() { @Override public void onValueChange(ValueChangeEvent<String> event) { openProject(event.getValue()); } }); // load project based on current url // TODO(sharon): Seems like a possible race condition here if the onValueChange // handler defined above gets called before the getSystemConfig call sets // userSettings. // The following line causes problems with GWT debugging, and commenting // it out doesn't seem to break things. //History.fireCurrentHistoryState(); } /* * Initializes all UI elements. */ private void initializeUi() { BlocklyPanel.initUi(); rpcStatusPopup = new RpcStatusPopup(); // Register services with RPC status popup rpcStatusPopup.register((ExtendedServiceProxy<?>) helpService); rpcStatusPopup.register((ExtendedServiceProxy<?>) projectService); rpcStatusPopup.register((ExtendedServiceProxy<?>) galleryService); rpcStatusPopup.register((ExtendedServiceProxy<?>) userInfoService); Window.setTitle(MESSAGES.titleYoungAndroid()); Window.enableScrolling(true); topPanel = new TopPanel(); statusPanel = new StatusPanel(); DockPanel mainPanel = new DockPanel(); mainPanel.add(topPanel, DockPanel.NORTH); // Create tab panel for subsequent tabs deckPanel = new DeckPanel() { @Override public final void onBrowserEvent(Event event) { switch (event.getTypeInt()) { case Event.ONCONTEXTMENU: event.preventDefault(); break; } } }; deckPanel.setAnimationEnabled(true); deckPanel.sinkEvents(Event.ONCONTEXTMENU); deckPanel.setStyleName("ode-DeckPanel"); // Projects tab VerticalPanel pVertPanel = new VerticalPanel(); pVertPanel.setWidth("100%"); pVertPanel.setSpacing(0); HorizontalPanel projectListPanel = new HorizontalPanel(); projectListPanel.setWidth("100%"); projectToolbar = new ProjectToolbar(); projectListPanel.add(ProjectListBox.getProjectListBox()); pVertPanel.add(projectToolbar); pVertPanel.add(projectListPanel); projectsTabIndex = deckPanel.getWidgetCount(); deckPanel.add(pVertPanel); // Design tab VerticalPanel dVertPanel = new VerticalPanel(); dVertPanel.setWidth("100%"); dVertPanel.setHeight("100%"); // Add the Code Navigation arrow // switchToBlocksButton = new VerticalPanel(); // switchToBlocksButton.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE); // switchToBlocksButton.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER); // switchToBlocksButton.setStyleName("ode-NavArrow"); // switchToBlocksButton.add(new Image(RIGHT_ARROW_IMAGE_URL)); // switchToBlocksButton.setWidth("25px"); // switchToBlocksButton.setHeight("100%"); // Add the Code Navigation arrow // switchToDesignerButton = new VerticalPanel(); // switchToDesignerButton.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE); // switchToDesignerButton.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER); // switchToDesignerButton.setStyleName("ode-NavArrow"); // switchToDesignerButton.add(new Image(LEFT_ARROW_IMAGE_URL)); // switchToDesignerButton.setWidth("25px"); // switchToDesignerButton.setHeight("100%"); designToolbar = new DesignToolbar(); dVertPanel.add(designToolbar); workColumns = new HorizontalPanel(); workColumns.setWidth("100%"); //workColumns.add(switchToDesignerButton); Box palletebox = PaletteBox.getPaletteBox(); palletebox.setWidth("222px"); workColumns.add(palletebox); Box viewerbox = ViewerBox.getViewerBox(); workColumns.add(viewerbox); workColumns.setCellWidth(viewerbox, "97%"); workColumns.setCellHeight(viewerbox, "97%"); structureAndAssets = new VerticalPanel(); structureAndAssets.setVerticalAlignment(VerticalPanel.ALIGN_TOP); // Only one of the SourceStructureBox and the BlockSelectorBox is visible // at any given time, according to whether we are showing the form editor // or the blocks editor. They share the same screen real estate. structureAndAssets.add(SourceStructureBox.getSourceStructureBox()); structureAndAssets.add(BlockSelectorBox.getBlockSelectorBox()); // initially not visible structureAndAssets.add(AssetListBox.getAssetListBox()); workColumns.add(structureAndAssets); Box propertiesbox = PropertiesBox.getPropertiesBox(); propertiesbox.setWidth("222px"); workColumns.add(propertiesbox); //switchToBlocksButton.setHeight("650px"); //workColumns.add(switchToBlocksButton); dVertPanel.add(workColumns); designTabIndex = deckPanel.getWidgetCount(); deckPanel.add(dVertPanel); // Gallery list tab VerticalPanel gVertPanel = new VerticalPanel(); gVertPanel.setWidth("100%"); gVertPanel.setSpacing(0); galleryListToolbar = new GalleryToolbar(); gVertPanel.add(galleryListToolbar); HorizontalPanel appListPanel = new HorizontalPanel(); appListPanel.setWidth("100%"); appListPanel.add(GalleryListBox.getGalleryListBox()); gVertPanel.add(appListPanel); galleryTabIndex = deckPanel.getWidgetCount(); deckPanel.add(gVertPanel); // Gallery app tab VerticalPanel aVertPanel = new VerticalPanel(); aVertPanel.setWidth("100%"); aVertPanel.setSpacing(0); galleryPageToolbar = new GalleryToolbar(); aVertPanel.add(galleryPageToolbar); HorizontalPanel appPanel = new HorizontalPanel(); appPanel.setWidth("100%"); appPanel.add(GalleryAppBox.getGalleryAppBox()); aVertPanel.add(appPanel); galleryAppTabIndex = deckPanel.getWidgetCount(); deckPanel.add(aVertPanel); // User Admin Panel VerticalPanel uaVertPanel = new VerticalPanel(); uaVertPanel.setWidth("100%"); uaVertPanel.setSpacing(0); HorizontalPanel adminUserListPanel = new HorizontalPanel(); adminUserListPanel.setWidth("100%"); adminUserListPanel.add(AdminUserListBox.getAdminUserListBox()); uaVertPanel.add(adminUserListPanel); userAdminTabIndex = deckPanel.getWidgetCount(); deckPanel.add(uaVertPanel); // KM: DEBUGGING BEGIN // User profile tab VerticalPanel uVertPanel = new VerticalPanel(); uVertPanel.setWidth("100%"); uVertPanel.setSpacing(0); HorizontalPanel userProfilePanel = new HorizontalPanel(); userProfilePanel.setWidth("100%"); userProfilePanel.add(ProfileBox.getUserProfileBox()); uVertPanel.add(userProfilePanel); userProfileTabIndex = deckPanel.getWidgetCount(); deckPanel.add(uVertPanel); // KM: DEBUGGING END // Private User Profile TabPanel VerticalPanel ppVertPanel = new VerticalPanel(); ppVertPanel.setWidth("100%"); ppVertPanel.setSpacing(0); HorizontalPanel privateUserProfileTabPanel = new HorizontalPanel(); privateUserProfileTabPanel.setWidth("100%"); privateUserProfileTabPanel.add(PrivateUserProfileTabPanel.getPrivateUserProfileTabPanel()); ppVertPanel.add(privateUserProfileTabPanel); privateUserProfileIndex = deckPanel.getWidgetCount(); deckPanel.add(ppVertPanel); // Moderation Page tab VerticalPanel mPVertPanel = new VerticalPanel(); mPVertPanel.setWidth("100%"); mPVertPanel.setSpacing(0); HorizontalPanel moderationPagePanel = new HorizontalPanel(); moderationPagePanel.setWidth("100%"); moderationPagePanel.add(ModerationPageBox.getModerationPageBox()); mPVertPanel.add(moderationPagePanel); moderationPageTabIndex = deckPanel.getWidgetCount(); deckPanel.add(mPVertPanel); // Debugging tab if (AppInventorFeatures.hasDebuggingView()) { Button dismissButton = new Button(MESSAGES.dismissButton()); dismissButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { if (currentView == DESIGNER) switchToDesignView(); else switchToProjectsView(); } }); ColumnLayout defaultLayout = new ColumnLayout("Default"); Column column = defaultLayout.addColumn(100); column.add(MessagesOutputBox.class, 300, false); column.add(OdeLogBox.class, 300, false); final WorkAreaPanel debuggingTab = new WorkAreaPanel(new OdeBoxRegistry(), defaultLayout); debuggingTab.add(dismissButton); debuggingTabIndex = deckPanel.getWidgetCount(); deckPanel.add(debuggingTab); // Hook the window resize event, so that we can adjust the UI. Window.addResizeHandler(new ResizeHandler() { @Override public void onResize(ResizeEvent event) { resizeWorkArea(debuggingTab); } }); // Call the window resized handler to get the initial sizes setup. Doing this in a deferred // command causes it to occur after all widgets' sizes have been computed by the browser. DeferredCommand.addCommand(new Command() { @Override public void execute() { resizeWorkArea(debuggingTab); } }); resizeWorkArea(debuggingTab); } // We do not select the designer tab here because at this point there is no current project. // Instead, we select the projects tab. If the user has a previously opened project, we will // open it and switch to the designer after the user settings are loaded. // Remember, the user may not have any projects at all yet. // Or, the user may have deleted their previously opened project. // ***** 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%"); // mainPanel.add(switchToDesignerButton, DockPanel.WEST); // mainPanel.add(switchToBlocksButton, DockPanel.EAST); //Commenting out for now to gain more space for the blocks editor mainPanel.add(statusPanel, DockPanel.SOUTH); mainPanel.setSize("100%", "100%"); RootPanel.get().add(mainPanel); // Add a handler to the RootPanel to keep track of Google Chrome Pinch Zooming and // handle relevant bugs. Chrome maps a Pinch Zoom to a MouseWheelEvent with the // control key pressed. RootPanel.get().addDomHandler(new MouseWheelHandler() { @Override public void onMouseWheel(MouseWheelEvent event) { if (event.isControlKeyDown()) { // Trip the appropriate flag in PZAwarePositionCallback when the page // is Pinch Zoomed. Note that this flag does not need to be removed when // the browser is un-zoomed because the patched function for determining // absolute position works in all circumstances. PZAwarePositionCallback.setPinchZoomed(true); } } }, MouseWheelEvent.getType()); // There is no sure-fire way of preventing people from accidentally navigating away from ODE // (e.g. by hitting the Backspace key). What we do need though is to make sure that people will // not lose any work because of this. We hook into the window closing event to detect the // situation. Window.addWindowClosingHandler(new Window.ClosingHandler() { @Override public void onWindowClosing(Window.ClosingEvent event) { onClosing(); } }); setupMotd(); } private void setupMotd() { AsyncCallback<Integer> callback = new AsyncCallback<Integer>() { @Override public void onFailure(Throwable caught) { OdeLog.log(MESSAGES.getMotdFailed()); } @Override public void onSuccess(Integer intervalSecs) { if (intervalSecs > 0) { topPanel.showMotd(); motdFetcher = new MotdFetcher(intervalSecs); motdFetcher.register((ExtendedServiceProxy<?>) projectService); motdFetcher.register((ExtendedServiceProxy<?>) userInfoService); } } }; getGetMotdService().getCheckInterval(callback); } /** * Returns the editor manager. * * @return {@link EditorManager} */ public EditorManager getEditorManager() { return editorManager; } /** * Returns the project manager. * * @return {@link ProjectManager} */ public ProjectManager getProjectManager() { return projectManager; } /** * Returns the project tool bar. * * @return {@link ProjectToolbar} */ public ProjectToolbar getProjectToolbar() { return projectToolbar; } /** * Returns the structureAndAssets panel. * * @return {@link VerticalPanel} */ public VerticalPanel getStructureAndAssets() { return structureAndAssets; } /** * Returns the workColumns panel. * * @return {@link HorizontalPanel} */ public HorizontalPanel getWorkColumns() { return workColumns; } /** * Returns the design tool bar. * * @return {@link DesignToolbar} */ public DesignToolbar getDesignToolbar() { return designToolbar; } /** * Returns the design tool bar. * * @return {@link DesignToolbar} */ public TopToolbar getTopToolbar() { return topToolbar; } /** * Set the location of the topToolBar. Called from * TopPanel(). We need a way to find it because the * blockly code needs to interact with the Connect-To * dropdown when a connection to a companion terminates. */ public void setTopToolbar(TopToolbar toolbar) { topToolbar = toolbar; } /** * Get an instance of the project information web service. * * @return project web service instance */ public ProjectServiceAsync getProjectService() { return projectService; } /** * Get an instance of the gallery information web service. * * @return gallery web service instance */ public GalleryServiceAsync getGalleryService() { return galleryService; } /** * Get an instance of the user information web service. * * @return user information web service instance */ public UserInfoServiceAsync getUserInfoService() { return userInfoService; } /** * Get an instance of the motd web service. * * @return motd web service instance */ public GetMotdServiceAsync getGetMotdService() { return getMotdService; } /** * Get an instance of the Admin Info service * * @return admin info service instance */ public AdminInfoServiceAsync getAdminInfoService() { return adminInfoService; } /** * Get an instance of the help web service. * * @return help service instance */ public HelpServiceAsync getHelpService() { return helpService; } /** * Get an instance of the launch RPC service. * * @return launch service instance */ public LaunchServiceAsync getLaunchService() { return launchService; } /** * Get an instance of the component web service. * * @return component web service instance */ public ComponentServiceAsync getComponentService() { return componentService; } /** * Set the current file editor. * * @param fileEditor the file editor, can be null. */ public void setCurrentFileEditor(FileEditor fileEditor) { currentFileEditor = fileEditor; if (currentFileEditor == null) { // nothing more we can do OdeLog.log("Setting current file editor to null"); return; } OdeLog.log("Ode: Setting current file editor to " + currentFileEditor.getFileId()); switchToDesignView(); if (!windowClosing) { userSettings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).changePropertyValue( SettingsConstants.GENERAL_SETTINGS_CURRENT_PROJECT_ID, "" + getCurrentYoungAndroidProjectId()); userSettings.saveSettings(null); } } /** * @return currently open FileEditor, or null if none */ public FileEditor getCurrentFileEditor() { return currentFileEditor; } /** * Returns the project root node for the current project, or null if there is no current project. * * @return project root node corresponding to current project */ public ProjectRootNode getCurrentYoungAndroidProjectRootNode() { if (currentFileEditor != null) { return currentFileEditor.getProjectRootNode(); } return null; } /** * Updates the modification date for the requested projected in the local * cached data structure based on the date received from the server. * @param date the date to update it to */ public void updateModificationDate(long projectId, long date) { Project project = getProjectManager().getProject(projectId); if (project != null) { project.setDateModified(date); } } /** * Returns the current project id, or 0 if there is no current project. * * @return the current project id */ public long getCurrentYoungAndroidProjectId() { if (currentFileEditor != null) { return currentFileEditor.getProjectId(); } return 0; } /** * Returns the current source node, or null if there is no current source node. * * @return the current source node */ public YoungAndroidSourceNode getCurrentYoungAndroidSourceNode() { if (currentFileEditor != null) { FileNode fileNode = currentFileEditor.getFileNode(); if (fileNode instanceof YoungAndroidSourceNode) { return (YoungAndroidSourceNode) fileNode; } } return null; } /** * Returns user account information. * * @return user account information */ public User getUser() { return user; } /** * Helper method to create push buttons. * * @param img image to shown on face of push button * @param tip text to show in tooltip * @return newly created push button */ public static PushButton createPushButton(ImageResource img, String tip, ClickHandler handler) { PushButton pb = new PushButton(new Image(img)); pb.addClickHandler(handler); pb.setTitle(tip); return pb; } private void resizeWorkArea(WorkAreaPanel workArea) { // Subtract 16px from width to account for vertical scrollbar FF3 likes to add workArea.onResize(Window.getClientWidth() - 16, Window.getClientHeight()); } private void onClosing() { // At this point, we aren't allowed to do any UI. windowClosing = true; if (motdFetcher != null) { motdFetcher.unregister((ExtendedServiceProxy<?>) projectService); motdFetcher.unregister((ExtendedServiceProxy<?>) userInfoService); } // Unregister services with RPC status popup. rpcStatusPopup.unregister((ExtendedServiceProxy<?>) helpService); rpcStatusPopup.unregister((ExtendedServiceProxy<?>) projectService); rpcStatusPopup.unregister((ExtendedServiceProxy<?>) userInfoService); // Save the user settings. userSettings.saveSettings(null); // Save all unsaved editors. editorManager.saveDirtyEditors(null); } /** * Creates, visually centers, and optionally displays the dialog box * that informs the user how to start learning about using App Inventor * or create a new project. * @param showDialog Convenience variable to show the created DialogBox. * @return The created and optionally displayed Dialog box. */ public DialogBox createNoProjectsDialog(boolean showDialog) { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(true, false); //DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.createNoProjectsDialogText()); Grid mainGrid = new Grid(2, 2); mainGrid.getCellFormatter().setAlignment(0, 0, HasHorizontalAlignment.ALIGN_CENTER, HasVerticalAlignment.ALIGN_MIDDLE); mainGrid.getCellFormatter().setAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER, HasVerticalAlignment.ALIGN_MIDDLE); mainGrid.getCellFormatter().setAlignment(1, 1, HasHorizontalAlignment.ALIGN_RIGHT, HasVerticalAlignment.ALIGN_MIDDLE); Image dialogImage = new Image(Ode.getImageBundle().androidGreenSmall()); Grid messageGrid = new Grid(2, 1); messageGrid.getCellFormatter().setAlignment(0, 0, HasHorizontalAlignment.ALIGN_JUSTIFY, HasVerticalAlignment.ALIGN_MIDDLE); messageGrid.getCellFormatter().setAlignment(1, 0, HasHorizontalAlignment.ALIGN_LEFT, HasVerticalAlignment.ALIGN_MIDDLE); Label messageChunk1 = new HTML(MESSAGES.createNoProjectsDialogMessage1()); messageChunk1.setWidth("23em"); Label messageChunk2 = new Label(MESSAGES.createNoprojectsDialogMessage2()); // Add the elements to the grids and DialogBox. messageGrid.setWidget(0, 0, messageChunk1); messageGrid.setWidget(1, 0, messageChunk2); mainGrid.setWidget(0, 0, dialogImage); mainGrid.setWidget(0, 1, messageGrid); dialogBox.setWidget(mainGrid); dialogBox.center(); if (showDialog) { dialogBox.show(); } return dialogBox; } /** * public entry for (re)displaying the welcome dialog box. * Bypass the "Do Not Show Again" feature. This is used by the * menu choice to explicitly show the dialog box. This lets * people who have dismissed the dialog to manually decide to * see it again. * */ public void showWelcomeDialog() { createWelcomeDialog(true); } /** * Possibly display the MIT App Inventor "Splash Screen" * * @param force Bypass the check to see if they have dimissed this version */ private void createWelcomeDialog(boolean force) { if (!shouldShowWelcomeDialog() && !force) { openProjectsTab(); return; } // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.createWelcomeDialogText()); dialogBox.setHeight(splashConfig.height + "px"); dialogBox.setWidth(splashConfig.width + "px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(splashConfig.content); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button ok = new Button(MESSAGES.createWelcomeDialogButton()); final CheckBox noshow = new CheckBox(MESSAGES.doNotShow()); ok.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); if (noshow.getValue()) { // User checked the box userSettings.getSettings(SettingsConstants.SPLASH_SETTINGS).changePropertyValue( SettingsConstants.SPLASH_SETTINGS_VERSION, "" + splashConfig.version); userSettings.saveSettings(null); } openProjectsTab(); } }); holder.add(ok); holder.add(noshow); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * Load and open the projects tab. */ private void openProjectsTab() { getProjectService().getProjects(new AsyncCallback<long[]>() { @Override public void onSuccess(long[] projectIds) { if (projectIds.length == 0 && !templateLoadingFlag) { createNoProjectsDialog(true); } } @Override public void onFailure(Throwable projectIds) { OdeLog.elog("Could not get project list"); } }); } /* * Check the user's setting to get the version of the Splash * Screen that they have seen. If they have seen this version (or greater) * then return false so they do not see it again. Return true to show it */ private boolean shouldShowWelcomeDialog() { if (splashConfig.version == 0) { // Never show splash if version is 0 return false; // Check first to avoid others unnecessary calls } String value = userSettings.getSettings(SettingsConstants.SPLASH_SETTINGS) .getPropertyValue(SettingsConstants.SPLASH_SETTINGS_VERSION); int uversion; if (value == null) { // Nothing stored uversion = 0; } else { uversion = Integer.parseInt(value); } if (uversion >= splashConfig.version) { return false; } else { return true; } } /** * Show a Survey Splash Screen to the user if they have not previously * acknowledged it. */ private void showSurveySplash() { // Create the UI elements of the DialogBox if (isReadOnly) { // Bypass the survey if we are read-only maybeShowSplash(); return; } final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.createWelcomeDialogText()); dialogBox.setHeight("200px"); dialogBox.setWidth("600px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.showSurveySplashMessage()); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button takesurvey = new Button(MESSAGES.showSurveySplashButtonNow()); takesurvey.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); // Update Splash Settings here userSettings.getSettings(SettingsConstants.SPLASH_SETTINGS).changePropertyValue( SettingsConstants.SPLASH_SETTINGS_SHOWSURVEY, "" + YaVersion.SPLASH_SURVEY); userSettings.saveSettings(null); takeSurvey(); // Open survey in a new window maybeShowSplash(); } }); holder.add(takesurvey); Button latersurvey = new Button(MESSAGES.showSurveySplashButtonLater()); latersurvey.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); maybeShowSplash(); } }); holder.add(latersurvey); Button neversurvey = new Button(MESSAGES.showSurveySplashButtonNever()); neversurvey.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); // Update Splash Settings here Settings settings = userSettings.getSettings(SettingsConstants.SPLASH_SETTINGS); settings.changePropertyValue(SettingsConstants.SPLASH_SETTINGS_SHOWSURVEY, "" + YaVersion.SPLASH_SURVEY); String declined = settings.getPropertyValue(SettingsConstants.SPLASH_SETTINGS_DECLINED); if (declined == null) declined = ""; // Shouldn't happen if (declined != "") declined += ","; declined += "" + YaVersion.SPLASH_SURVEY; // Record that we declined this survey settings.changePropertyValue(SettingsConstants.SPLASH_SETTINGS_DECLINED, declined); userSettings.saveSettings(null); maybeShowSplash(); } }); holder.add(neversurvey); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } private void maybeShowSplash() { if (AppInventorFeatures.showSplashScreen() && !isReadOnly) { createWelcomeDialog(false); } else { openProjectsTab(); } } // Display the Survey and/or Normal Splash Screens // (if enabled). This function is called out of SplashSettings.java // after the userSettings object is loaded (above) and parsed. public void showSplashScreens() { boolean showSplash = false; if (AppInventorFeatures.showSurveySplashScreen()) { int nvalue = 0; String value = userSettings.getSettings(SettingsConstants.SPLASH_SETTINGS) .getPropertyValue(SettingsConstants.SPLASH_SETTINGS_SHOWSURVEY); if (value != null) { nvalue = Integer.parseInt(value); } if (nvalue < YaVersion.SPLASH_SURVEY) { showSurveySplash(); } else { showSplash = true; } } else { showSplash = true; } if (showSplash) { maybeShowSplash(); } } /** * Show a Dialog Box when we receive an SC_PRECONDITION_FAILED * response code to any Async RPC call. This is a signal that * either our session has expired, or our login cookie has otherwise * become invalid. This is a fatal error and the user should not * be permitted to continue (many ignore the red error bar and keep * working, in vain). So now when this happens, we put up this * modal dialog box which cannot be dismissed. Instead it presents * just one option, a "Reload" button which reloads the browser. * This should trigger a re-authentication (or in the case of an * App Inventor upgrade trigging the problem, the loading of newer * code). */ public void sessionDead() { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.invalidSessionDialogText()); dialogBox.setWidth("400px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.sessionDead()); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button reloadSession = new Button(MESSAGES.reloadWindow()); reloadSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); reloadWindow(true); } }); holder.add(reloadSession); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * Show a Warning Dialog box when another login session has been * created. The user is then given two choices. They can either * close this session of App Inventor, which will close the current * window, or they can click "Take Over" which will reload this * window effectively making it the latest login and invalidating * all other sessions. * * We are called from OdeAsyncCallback when we detect that our * session has been invalidated. */ public void invalidSessionDialog() { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.invalidSessionDialogText()); dialogBox.setHeight("200px"); dialogBox.setWidth("800px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.invalidSessionDialogMessage()); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button closeSession = new Button(MESSAGES.invalidSessionDialogButtonEnd()); closeSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); finalDialog(); } }); holder.add(closeSession); Button reloadSession = new Button(MESSAGES.invalidSessionDialogButtonCurrent()); reloadSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); reloadWindow(false); } }); holder.add(reloadSession); Button continueSession = new Button(MESSAGES.invalidSessionDialogButtonContinue()); continueSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); bashWarningDialog(); } }); holder.add(continueSession); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * The user has chosen to continue a session even though * others are still active. This risks damaging (bashing) projects. * So before we proceed, we provide a stern warning. If they press * "Continue" we set their sessionId to "force" which is recognized * by the backend as a sessionId that should always match. This is * safe because normal sessionIds are UUIDs which are always longer * then the word "force." I know this is a bit kludgey, but by doing * it this way we don't have to change the RPC interface which makes * releasing this code non-disruptive to people using App Inventor * during the release. * * If the user selects "Cancel" we take them back to the * invalidSessionDialog. */ private void bashWarningDialog() { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.bashWarningDialogText()); dialogBox.setHeight("200px"); dialogBox.setWidth("800px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.bashWarningDialogMessage()); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button continueSession = new Button(MESSAGES.bashWarningDialogButtonContinue()); continueSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); sessionId = "force"; // OK, over-ride in place! // Because we ultimately got here from a failure in the save function... ChainableCommand cmd = new SaveAllEditorsCommand(null); cmd.startExecuteChain(Tracking.PROJECT_ACTION_SAVE_YA, getCurrentYoungAndroidProjectRootNode()); // Will now go back to our regularly scheduled main loop } }); holder.add(continueSession); Button cancelSession = new Button(MESSAGES.bashWarningDialogButtonNo()); cancelSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); invalidSessionDialog(); } }); holder.add(cancelSession); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * The "Final" Dialog box. When a user chooses to end their session * due to a conflicting login, we should show this dialog which is modal * and has no exit! My preference would have been to close the window * altogether, but the browsers won't let javascript code close windows * that it didn't open itself (like the main window). I also tried to * use document.write() to write replacement HTML but that caused errors * in Firefox and strange behavior in Chrome. So we do this... * * We are called from invalidSessionDialog() (above). */ private void finalDialog() { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.finalDialogText()); dialogBox.setHeight("100px"); dialogBox.setWidth("400px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.finalDialogMessage()); message.setStyleName("DialogBox-message"); DialogBoxContents.add(message); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * corruptionDialog -- Put up a dialog box explaining that we detected corruption * while reading in a project file. There is no continuing once this happens. * */ void corruptionDialog() { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.corruptionDialogText()); dialogBox.setHeight("100px"); dialogBox.setWidth("400px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.corruptionDialogMessage()); message.setStyleName("DialogBox-message"); DialogBoxContents.add(message); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } public void blocksTruncatedDialog(final long projectId, final String fileId, final String content, final OdeAsyncCallback callback) { final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.blocksTruncatedDialogText()); dialogBox.setHeight("150px"); dialogBox.setWidth("600px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); String[] fileParts = fileId.split("/"); String screenNameParts = fileParts[fileParts.length - 1]; final String screenName = screenNameParts.split("\\.")[0]; // Get rid of the .bky part final String userEmail = user.getUserEmail(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML(MESSAGES.blocksTruncatedDialogMessage().replace("%1", screenName)); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); final Button continueSession = new Button(MESSAGES.blocksTruncatedDialogButtonSave()); continueSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); // call save2 again, this time with force = true so the empty workspace will be written getProjectService().save2(getSessionId(), projectId, fileId, true, content, callback); } }); holder.add(continueSession); final Button cancelSession = new Button(MESSAGES.blocksTruncatedDialogButtonNoSave()); final OdeAsyncCallback<Void> logReturn = new OdeAsyncCallback<Void>() { @Override public void onSuccess(Void result) { reloadWindow(false); } }; cancelSession.addClickListener(new ClickListener() { public void onClick(Widget sender) { // Note: We do *not* remove the dialog, this locks the UI up (our intent) // Wait for a few seconds for other I/O to complete cancelSession.setEnabled(false); // Disable button to prevent further clicking continueSession.setEnabled(false); // This one as well Timer t = new Timer() { int count = 5; @Override public void run() { if (count > 0) { HTML html = (HTML) ((VerticalPanel) dialogBox.getWidget()).getWidget(0); html.setHTML(MESSAGES.blocksTruncatedDialogButtonHTML().replace("%1", "" + count)); count -= 1; } else { this.cancel(); getProjectService().log("Disappearing Blocks: ProjectId = " + projectId + " fileId = " + fileId + " User = " + userEmail, logReturn); } } }; t.scheduleRepeating(1000); // Run every second } }); holder.add(cancelSession); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * Display a Dialog box that explains that you cannot connect a * device or the emulator to App Inventor until you have a project * selected. */ private void wontConnectDialog() { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.noprojectDialogTitle()); dialogBox.setHeight("100px"); dialogBox.setWidth("400px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML("<p>" + MESSAGES.noprojectDuringConnect() + "</p>"); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button okButton = new Button("OK"); okButton.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); } }); holder.add(okButton); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * This dialog is showned if an account is disabled. It is * completely modal with no escape. The provided URL is displayed in * an iframe, so it can be tailored to each person whose account is * disabled. * * @param Url the Url to display in the dialog box. */ public void disabledAccountDialog(String Url) { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(MESSAGES.accountDisabledMessage()); dialogBox.setHeight("700px"); dialogBox.setWidth("700px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML( "<iframe src=\"" + Url + "\" style=\"border: 0; width: 680px; height: 660px;\"></iframe>"); message.setStyleName("DialogBox-message"); DialogBoxContents.add(message); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * Display a generic warning dialog box. * This method is public because it is intended to be used from other * parts of the client GWT side system. * * Note: We expect our caller to internationalize the messages to be * displayed. * * @param title The title for the dialog box * @param message The message to display * @param buttonString the name of the button, i.e., "OK" */ public void warningDialog(String title, String messageString, String buttonString) { // Create the UI elements of the DialogBox final DialogBox dialogBox = new DialogBox(false, true); // DialogBox(autohide, modal) dialogBox.setStylePrimaryName("ode-DialogBox"); dialogBox.setText(title); dialogBox.setHeight("100px"); dialogBox.setWidth("400px"); dialogBox.setGlassEnabled(true); dialogBox.setAnimationEnabled(true); dialogBox.center(); VerticalPanel DialogBoxContents = new VerticalPanel(); HTML message = new HTML("<p>" + messageString + "</p>"); message.setStyleName("DialogBox-message"); FlowPanel holder = new FlowPanel(); Button okButton = new Button(buttonString); okButton.addClickListener(new ClickListener() { public void onClick(Widget sender) { dialogBox.hide(); } }); holder.add(okButton); DialogBoxContents.add(message); DialogBoxContents.add(holder); dialogBox.setWidget(DialogBoxContents); dialogBox.show(); } /** * Is it OK to connect a device/emulator. Returns true if so false * otherwise. * * Determination is made based on whether or not a project is * selected. * * @return boolean */ public boolean okToConnect() { if (getCurrentYoungAndroidProjectId() == 0) { wontConnectDialog(); return false; } else { return true; } } /** * recordCorruptProject -- Record that we received a corrupt read. This * may or may not work depending on the reason why we received a corrupt * file. If the network just went down, then obviously we won't be able * to use the network to report this problem. However if the corruption * was due to a proxy mangling data (perhaps in the name of censorship) * then this will likely work. We'll see.... (JIS) * */ public void recordCorruptProject(long projectId, String fileId, String message) { getProjectService().recordCorruption(projectId, fileId, message, new OdeAsyncCallback<Void>("") { // No failure message @Override public void onSuccess(Void result) { // do nothing } }); } /** * generateNonce() -- Generate a unique String value * this value is used to reference a built APK without * requiring explicit authentication. * * @return nonce */ public String generateNonce() { int v = random.nextInt(1000000); nonce = Integer.toString(v, 36); // Base 36 string return nonce; } public String getSessionId() { return sessionId; } /* * getNonce() -- return a previously generated nonce. * * @return nonce */ public String getNonce() { return nonce; } public boolean isReadOnly() { return isReadOnly; } // This is called from AdminUserList when we are switching users // See the comment there... public void setReadOnly() { isReadOnly = true; } // Code to lock out certain screen and project switching code // These are locked out while files are being saved // lockScreens(true) is called from EditorManager when it // is about to call saveDirtyEditors() and then cleared // in the afterSaving command called when saveDirtyEditors // is finished. public boolean screensLocked() { return screensLocked; } public void lockScreens(boolean value) { if (value) { OdeLog.log("Locking Screens"); } else { OdeLog.log("Unlocking Screens"); } screensLocked = value; } // Native code to set the top level rendezvousServer variable // where blockly code can easily find it. private native void setRendezvousServer(String server) /*-{ top.rendezvousServer = server; }-*/; // Native code to open a new window (or tab) to display the // desired survey. The value below "http://web.mit.edu" is just // a plug value. You should insert your own as appropriate. private native void takeSurvey() /*-{ $wnd.open("http://web.mit.edu"); }-*/; // Making this public in case we need something like this elsewhere public static native String generateUuid() /*-{ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); }-*/; public static native void reloadWindow(boolean full) /*-{ if (full) { top.location.replace(top.location.origin); } else { top.location.reload(); } }-*/; }