// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client.editor; import com.google.appinventor.client.Ode; import com.google.appinventor.client.explorer.project.Project; import com.google.appinventor.client.output.OdeLog; import com.google.appinventor.client.settings.Settings; import com.google.appinventor.shared.settings.SettingsConstants; import com.google.appinventor.client.settings.project.ProjectSettings; import com.google.appinventor.shared.rpc.project.ProjectRootNode; import com.google.common.collect.Maps; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.DeckPanel; import com.google.gwt.user.client.ui.VerticalPanel; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Abstract superclass for all project editors. * Each ProjectEditor is associated with a single project and may have multiple * FileEditors open in a DeckPanel. * * TODO(sharon): consider merging this into YaProjectEditor, since we now * only have one type of project editor. * * @author lizlooney@google.com (Liz Looney) */ public abstract class ProjectEditor extends Composite { protected final ProjectRootNode projectRootNode; protected final long projectId; protected final Project project; // Invariants: openFileEditors, fileIds, and deckPanel contain corresponding // elements, i.e., if a FileEditor is in openFileEditors, its fileid should be // in fileIds and the FileEditor should be in deckPanel. If selectedFileEditor // is non-null, it is one of the file editors in openFileEditors and the // one currently showing in deckPanel. private final Map<String, FileEditor> openFileEditors; protected final List<String> fileIds; private final HashMap<String,String> locationHashMap = new HashMap<String,String>(); private final DeckPanel deckPanel; private FileEditor selectedFileEditor; /** * Creates a {@code ProjectEditor} instance. * * @param projectRootNode the project root node */ public ProjectEditor(ProjectRootNode projectRootNode) { this.projectRootNode = projectRootNode; projectId = projectRootNode.getProjectId(); project = Ode.getInstance().getProjectManager().getProject(projectId); openFileEditors = Maps.newHashMap(); fileIds = new ArrayList<String>(); deckPanel = new DeckPanel(); VerticalPanel panel = new VerticalPanel(); panel.add(deckPanel); deckPanel.setSize("100%", "100%"); panel.setSize("100%", "100%"); initWidget(panel); // Note: I'm not sure that the setSize call below does anything useful. setSize("100%", "100%"); } /** * Processes the project before loading into the project editor. * To do any any pre-processing of the Project * Calls the loadProject() after prepareProject() is fully executed. * Currently, prepareProject loads all external components associated with project. */ public abstract void processProject(); /** * Called when the ProjectEditor widget is loaded after having been hidden. * Subclasses must implement this method, taking responsiblity for causing * the onShow method of the selected file editor to be called and for updating * any other UI elements related to showing the project editor. */ protected abstract void onShow(); /** * Called when the ProjectEditor widget is about to be unloaded. Subclasses * must implement this method, taking responsiblity for causing the onHide * method of the selected file editor to be called and for updating any * other UI elements related to hiding the project editor. */ protected abstract void onHide(); /** * Adds a file editor to this project editor. * * @param fileEditor file editor to add */ public final void addFileEditor(FileEditor fileEditor) { String fileId = fileEditor.getFileId(); openFileEditors.put(fileId, fileEditor); fileIds.add(fileId); deckPanel.add(fileEditor); } /** * Inserts a file editor in this editor at the specified index. * * @param fileEditor file editor to insert * @param beforeIndex the index before which fileEditor will be inserted */ public final void insertFileEditor(FileEditor fileEditor, int beforeIndex) { String fileId = fileEditor.getFileId(); openFileEditors.put(fileId, fileEditor); fileIds.add(beforeIndex, fileId); deckPanel.insert(fileEditor, beforeIndex); OdeLog.log("Inserted file editor for " + fileEditor.getFileId() + " at pos " + beforeIndex); } /** * Selects the given file editor in the deck panel and calls its onShow() * method. Calls onHide() for a previously selected file editor if there was * one (and it wasn't the same one). * * Note: all actions that cause the selected file editor to change should * be going through DesignToolbar.SwitchScreenAction.execute(), which calls * this method. If you're thinking about calling this method directly from * somewhere else, please reconsider! * * @param fileEditor file editor to select */ public final void selectFileEditor(FileEditor fileEditor) { int index = deckPanel.getWidgetIndex(fileEditor); if (index == -1) { if (fileEditor != null) { OdeLog.wlog("Can't find widget for fileEditor " + fileEditor.getFileId()); } else { OdeLog.wlog("Not expecting selectFileEditor(null)"); } } OdeLog.log("ProjectEditor: got selectFileEditor for " + ((fileEditor == null) ? null : fileEditor.getFileId()) + " selectedFileEditor is " + ((selectedFileEditor == null) ? null : selectedFileEditor.getFileId())); if (selectedFileEditor != null && selectedFileEditor != fileEditor) { selectedFileEditor.onHide(); } // Note that we still want to do the following statements even if // selectedFileEdtior == fileEditor already. This handles the case of switching back // to a previously opened project from another project. selectedFileEditor = fileEditor; deckPanel.showWidget(index); selectedFileEditor.onShow(); } /** * Returns the file editor for the given file ID. * * @param fileId file ID of the file */ public final FileEditor getFileEditor(String fileId) { return openFileEditors.get(fileId); } /** * Returns the set of open file editors */ public final Iterable<FileEditor> getOpenFileEditors() { return Collections.unmodifiableCollection(openFileEditors.values()); } /** * Returns the currently selected file editor */ protected final FileEditor getSelectedFileEditor() { return selectedFileEditor; } /** * Closes the file editors for the given file IDs, without saving. * This is used when the files are about to be deleted. If * selectedFileEditor is closed, sets selectedFileEditor to null. * * @param closeFileIds file IDs of the files to be closed */ public final void closeFileEditors(String[] closeFileIds) { for (String fileId : closeFileIds) { FileEditor fileEditor = openFileEditors.remove(fileId); if (fileEditor == null) { OdeLog.elog("File editor is unexpectedly null for " + fileId); continue; } int index = deckPanel.getWidgetIndex(fileEditor); fileIds.remove(index); deckPanel.remove(fileEditor); if (selectedFileEditor == fileEditor) { selectedFileEditor = null; } fileEditor.onClose(); } } /** * Returns the value of a project settings property. * * @param category property category * @param name property name * @return the property value */ public final String getProjectSettingsProperty(String category, String name) { ProjectSettings projectSettings = project.getSettings(); Settings settings = projectSettings.getSettings(category); return settings.getPropertyValue(name); } /** * Changes the value of a project settings property. * * @param category property category * @param name property name * @param newValue new property value */ public final void changeProjectSettingsProperty(String category, String name, String newValue) { ProjectSettings projectSettings = project.getSettings(); Settings settings = projectSettings.getSettings(category); String currentValue = settings.getPropertyValue(name); if (!newValue.equals(currentValue)) { settings.changePropertyValue(name, newValue); Ode.getInstance().getEditorManager().scheduleAutoSave(projectSettings); } } /** * Keep track of components that require the * "android.permission.ACCESS_FINE_LOCATION" (and related * permissions). This code is in particular for use of the WebViewer * component. The WebViewer exports the Javascript location * API. However it cannot be used by an app with location * permissions. Each WebViewer has a "UsesLocation" property which * is only available from the designer. Each WebViewer then * registers its value here. Each time this hashtable is updated we * recompute whether or not location permission is needed based on a * logical OR of all of the WebViwer components registered. Note: * Even if no WebViewer component requires location permisson, other * components, such as the LocationSensor may require it. That is * handled via the @UsesPermissions mechanism and is independent of * this code. * * @param componentName The name of the component registering location permission * @param newVlue either "True" or "False" indicating whether permission is need. */ public final void recordLocationSetting(String componentName, String newValue) { OdeLog.log("ProjectEditor: recordLocationSetting(" + componentName + "," + newValue + ")"); locationHashMap.put(componentName, newValue); recomputeLocationPermission(); } private final void recomputeLocationPermission() { String usesLocation = "False"; for (String c : locationHashMap.values()) { OdeLog.log("ProjectEditor:recomputeLocationPermission: " + c); if (c.equals("True")) { usesLocation = "True"; break; } } changeProjectSettingsProperty(SettingsConstants.PROJECT_YOUNG_ANDROID_SETTINGS, SettingsConstants.YOUNG_ANDROID_SETTINGS_USES_LOCATION, usesLocation); } public void clearLocation(String componentName) { OdeLog.log("ProjectEditor:clearLocation: clearing " + componentName); locationHashMap.remove(componentName); recomputeLocationPermission(); } /** * Notification that the file with the given file ID has been saved. * * @param fileId file ID of the file that was saved */ public final void onSave(String fileId) { FileEditor fileEditor = openFileEditors.get(fileId); if (fileEditor != null) { fileEditor.onSave(); } } // GWT Widget methods @Override protected void onLoad() { // onLoad is called immediately after a widget becomes attached to the browser's document. // onLoad will be called both when a project is opened the first time and when an // already-opened project is re-opened. // This is different from the ProjectEditor method loadProject, which is called to load the // project just after the editor is created. OdeLog.log("ProjectEditor: got onLoad for project " + projectId); super.onLoad(); onShow(); } @Override protected void onUnload() { // onUnload is called immediately before a widget becomes detached from the browser's document. OdeLog.log("ProjectEditor: got onUnload for project " + projectId); super.onUnload(); onHide(); } }