From b9dd664f2996e4090603e3fc224b98453a49f759 Mon Sep 17 00:00:00 2001 From: Raphael Moll Date: Mon, 14 Jun 2010 15:31:21 -0700 Subject: [PATCH] ADT: Editor for export.properties in export-projects. This is an initial framework that needs to be refined. Change-Id: I2cd8a7708c30826075de076d2b5826ed8af77eb9 --- .../plugins/com.android.ide.eclipse.adt/plugin.xml | 8 + .../adt/internal/editors/AndroidTextEditor.java | 572 +++++++++++++++++++++ .../export/AbstractPropertiesFieldsPart.java | 351 +++++++++++++ .../adt/internal/editors/export/ExportEditor.java | 107 ++++ .../internal/editors/export/ExportFieldsPart.java | 100 ++++ .../internal/editors/export/ExportLinksPart.java | 121 +++++ .../editors/export/ExportPropertiesPage.java | 113 ++++ 7 files changed, 1372 insertions(+) create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index 130abe8c8..78ebac379 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -440,6 +440,14 @@ name="Android Manifest Editor"> + + + * It is designed to work with a {@link TextEditor} that will display a text file. + *
+ * Derived classes must implement createFormPages to create the forms before the + * source editor. This can be a no-op if desired. + */ +public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener { + + /** Preference name for the current page of this file */ + private static final String PREF_CURRENT_PAGE = "_current_page"; + + /** Id string used to create the Android SDK browser */ + private static String BROWSER_ID = "android"; // $NON-NLS-1$ + + /** Page id of the XML source editor, used for switching tabs programmatically */ + public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$ + + /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */ + public static final int TEXT_WIDTH_HINT = 50; + + /** Page index of the text editor (always the last page) */ + private int mTextPageIndex; + + /** The text editor */ + private TextEditor mTextEditor; + + /** flag set during page creation */ + private boolean mIsCreatingPage = false; + + private IDocument mDocument; + + /** + * Creates a form editor. + */ + public AndroidTextEditor() { + super(); + } + + // ---- Abstract Methods ---- + + /** + * Creates the various form pages. + *

+ * Derived classes must implement this to add their own specific tabs. + */ + abstract protected void createFormPages(); + + /** + * Called by the base class {@link AndroidTextEditor} once all pages (custom form pages + * as well as text editor page) have been created. This give a chance to deriving + * classes to adjust behavior once the text page has been created. + */ + protected void postCreatePages() { + // Nothing in the base class. + } + + /** + * Subclasses should override this method to process the new text model. + * This is called after the document has been edited. + * + * The base implementation is empty. + * + * @param event Specification of changes applied to document. + */ + protected void onDocumentChanged(DocumentEvent event) { + // pass + } + + // ---- Base Class Overrides, Interfaces Implemented ---- + + /** + * Creates the pages of the multi-page editor. + */ + @Override + protected void addPages() { + createAndroidPages(); + selectDefaultPage(null /* defaultPageId */); + } + + /** + * Creates the page for the Android Editors + */ + protected void createAndroidPages() { + mIsCreatingPage = true; + createFormPages(); + createTextEditor(); + createUndoRedoActions(); + postCreatePages(); + mIsCreatingPage = false; + } + + /** + * Returns whether the editor is currently creating its pages. + */ + public boolean isCreatingPages() { + return mIsCreatingPage; + } + + /** + * Creates undo redo actions for the editor site (so that it works for any page of this + * multi-page editor) by re-using the actions defined by the {@link TextEditor} + * (aka the XML text editor.) + */ + private void createUndoRedoActions() { + IActionBars bars = getEditorSite().getActionBars(); + if (bars != null) { + IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId()); + bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action); + + action = mTextEditor.getAction(ActionFactory.REDO.getId()); + bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action); + + bars.updateActionBars(); + } + } + + /** + * Selects the default active page. + * @param defaultPageId the id of the page to show. If null the editor attempts to + * find the default page in the properties of the {@link IResource} object being edited. + */ + protected void selectDefaultPage(String defaultPageId) { + if (defaultPageId == null) { + if (getEditorInput() instanceof IFileEditorInput) { + IFile file = ((IFileEditorInput) getEditorInput()).getFile(); + + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, + getClass().getSimpleName() + PREF_CURRENT_PAGE); + String pageId; + try { + pageId = file.getPersistentProperty(qname); + if (pageId != null) { + defaultPageId = pageId; + } + } catch (CoreException e) { + // ignored + } + } + } + + if (defaultPageId != null) { + try { + setActivePage(Integer.parseInt(defaultPageId)); + } catch (Exception e) { + // We can get NumberFormatException from parseInt but also + // AssertionError from setActivePage when the index is out of bounds. + // Generally speaking we just want to ignore any exception and fall back on the + // first page rather than crash the editor load. Logging the error is enough. + AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId); + } + } + } + + /** + * Removes all the pages from the editor. + */ + protected void removePages() { + int count = getPageCount(); + for (int i = count - 1 ; i >= 0 ; i--) { + removePage(i); + } + } + + /** + * Overrides the parent's setActivePage to be able to switch to the xml editor. + * + * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page. + * This is needed because the editor doesn't actually derive from IFormPage and thus + * doesn't have the get-by-page-id method. In this case, the method returns null since + * IEditorPart does not implement IFormPage. + */ + @Override + public IFormPage setActivePage(String pageId) { + if (pageId.equals(TEXT_EDITOR_ID)) { + super.setActivePage(mTextPageIndex); + return null; + } else { + return super.setActivePage(pageId); + } + } + + + /** + * Notifies this multi-page editor that the page with the given id has been + * activated. This method is called when the user selects a different tab. + * + * @see MultiPageEditorPart#pageChange(int) + */ + @Override + protected void pageChange(int newPageIndex) { + super.pageChange(newPageIndex); + + // Do not record page changes during creation of pages + if (mIsCreatingPage) { + return; + } + + if (getEditorInput() instanceof IFileEditorInput) { + IFile file = ((IFileEditorInput) getEditorInput()).getFile(); + + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, + getClass().getSimpleName() + PREF_CURRENT_PAGE); + try { + file.setPersistentProperty(qname, Integer.toString(newPageIndex)); + } catch (CoreException e) { + // ignore + } + } + } + + /** + * Notifies this listener that some resource changes + * are happening, or have already happened. + * + * Closes all project files on project close. + * @see IResourceChangeListener + */ + public void resourceChanged(final IResourceChangeEvent event) { + if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { + Display.getDefault().asyncExec(new Runnable() { + public void run() { + IWorkbenchPage[] pages = getSite().getWorkbenchWindow() + .getPages(); + for (int i = 0; i < pages.length; i++) { + if (((FileEditorInput)mTextEditor.getEditorInput()) + .getFile().getProject().equals( + event.getResource())) { + IEditorPart editorPart = pages[i].findEditor(mTextEditor + .getEditorInput()); + pages[i].closeEditor(editorPart, true); + } + } + } + }); + } + } + + /** + * Initializes the editor part with a site and input. + *

+ * Checks that the input is an instance of {@link IFileEditorInput}. + * + * @see FormEditor + */ + @Override + public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException { + if (!(editorInput instanceof IFileEditorInput)) + throw new PartInitException("Invalid Input: Must be IFileEditorInput"); + super.init(site, editorInput); + } + + /** + * Removes attached listeners. + * + * @see WorkbenchPart + */ + @Override + public void dispose() { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); + + super.dispose(); + } + + /** + * Commit all dirty pages then saves the contents of the text editor. + *

+ * This works by committing all data to the XML model and then + * asking the Structured XML Editor to save the XML. + * + * @see IEditorPart + */ + @Override + public void doSave(IProgressMonitor monitor) { + commitPages(true /* onSave */); + + // The actual "save" operation is done by the Structured XML Editor + getEditor(mTextPageIndex).doSave(monitor); + } + + /* (non-Javadoc) + * Saves the contents of this editor to another object. + *

+ * Subclasses must override this method to implement the open-save-close lifecycle + * for an editor. For greater details, see IEditorPart + *

+ * + * @see IEditorPart + */ + @Override + public void doSaveAs() { + commitPages(true /* onSave */); + + IEditorPart editor = getEditor(mTextPageIndex); + editor.doSaveAs(); + setPageText(mTextPageIndex, editor.getTitle()); + setInput(editor.getEditorInput()); + } + + /** + * Commits all dirty pages in the editor. This method should + * be called as a first step of a 'save' operation. + *

+ * This is the same implementation as in {@link FormEditor} + * except it fixes two bugs: a cast to IFormPage is done + * from page.get(i) before being tested with instanceof. + * Another bug is that the last page might be a null pointer. + *

+ * The incorrect casting makes the original implementation crash due + * to our {@link StructuredTextEditor} not being an {@link IFormPage} + * so we have to override and duplicate to fix it. + * + * @param onSave true if commit is performed as part + * of the 'save' operation, false otherwise. + * @since 3.3 + */ + @Override + public void commitPages(boolean onSave) { + if (pages != null) { + for (int i = 0; i < pages.size(); i++) { + Object page = pages.get(i); + if (page != null && page instanceof IFormPage) { + IFormPage form_page = (IFormPage) page; + IManagedForm managed_form = form_page.getManagedForm(); + if (managed_form != null && managed_form.isDirty()) { + managed_form.commit(onSave); + } + } + } + } + } + + /* (non-Javadoc) + * Returns whether the "save as" operation is supported by this editor. + *

+ * Subclasses must override this method to implement the open-save-close lifecycle + * for an editor. For greater details, see IEditorPart + *

+ * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return false; + } + + // ---- Local methods ---- + + + /** + * Helper method that creates a new hyper-link Listener. + * Used by derived classes which need active links in {@link FormText}. + *

+ * This link listener handles two kinds of URLs: + *

+ * + * @return A new hyper-link listener for FormText to use. + */ + public final IHyperlinkListener createHyperlinkListener() { + return new HyperlinkAdapter() { + /** + * Switch to the page corresponding to the link that has just been clicked. + * For this purpose, the HREF of the <a> tags above is the page ID to switch to. + */ + @Override + public void linkActivated(HyperlinkEvent e) { + super.linkActivated(e); + String link = e.data.toString(); + if (link.startsWith("http") || //$NON-NLS-1$ + link.startsWith("file:/")) { //$NON-NLS-1$ + openLinkInBrowser(link); + } else if (link.startsWith("page:")) { //$NON-NLS-1$ + // Switch to an internal page + setActivePage(link.substring(5 /* strlen("page:") */)); + } + } + }; + } + + /** + * Open the http link into a browser + * + * @param link The URL to open in a browser + */ + private void openLinkInBrowser(String link) { + try { + IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance(); + wbs.createBrowser(BROWSER_ID).openURL(new URL(link)); + } catch (PartInitException e1) { + // pass + } catch (MalformedURLException e1) { + // pass + } + } + + /** + * Creates the XML source editor. + *

+ * Memorizes the index page of the source editor (it's always the last page, but the number + * of pages before can change.) + *
+ * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it. + * Finally triggers modelChanged() on the model listener -- derived classes can use this + * to initialize the model the first time. + *

+ * Called only once after createFormPages. + */ + private void createTextEditor() { + try { + mTextEditor = new TextEditor(); + int index = addPage(mTextEditor, getEditorInput()); + mTextPageIndex = index; + setPageText(index, mTextEditor.getTitle()); + + IDocumentProvider provider = mTextEditor.getDocumentProvider(); + mDocument = provider.getDocument(getEditorInput()); + + mDocument.addDocumentListener(new IDocumentListener() { + public void documentChanged(DocumentEvent event) { + onDocumentChanged(event); + } + + public void documentAboutToBeChanged(DocumentEvent event) { + // ignore + } + }); + + + } catch (PartInitException e) { + ErrorDialog.openError(getSite().getShell(), + "Android Text Editor Error", null, e.getStatus()); + } + } + + /** + * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to + * the current file input. + *

+ * All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}. + * The actual document instance is a {@link SynchronizableDocument}, which creates a lock + * around read/set operations. The base API provided by {@link IDocument} provides ways to + * manipulate the document line per line or as a bulk. + */ + public IDocument getDocument() { + return mDocument; + } + + /** + * Returns the {@link IProject} for the edited file. + */ + public IProject getProject() { + if (mTextEditor != null) { + IEditorInput input = mTextEditor.getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + IFile inputFile = fileInput.getFile(); + + if (inputFile != null) { + return inputFile.getProject(); + } + } + } + + return null; + } + + /** + * Runs the given operation in the context of a document RewriteSession. + * Takes care of properly starting and stopping the operation. + *

+ * The operation itself should just access {@link #getDocument()} and use the + * normal document's API to manipulate it. + * + * @see #getDocument() + */ + public void wrapRewriteSession(Runnable operation) { + if (mDocument instanceof IDocumentExtension4) { + IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument; + + DocumentRewriteSession session = null; + try { + session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL); + + operation.run(); + } catch(IllegalStateException e) { + AdtPlugin.log(e, "wrapRewriteSession failed"); + e.printStackTrace(); + } finally { + if (session != null) { + doc4.stopRewriteSession(session); + } + } + + } else { + // Not an IDocumentExtension4? Unlikely. Try the operation anyway. + operation.run(); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java new file mode 100755 index 000000000..06169d27b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; +import com.android.sdklib.SdkConstants; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +import java.util.HashMap; +import java.util.HashSet; + +/** + * Section part for editing fields of a properties file in an Export editor. + *

+ * This base class is intended to be derived and customized. + */ +abstract class AbstractPropertiesFieldsPart extends ManifestSectionPart { + + private final HashMap mNameToField = new HashMap(); + + private ExportEditor mEditor; + + private boolean mInternalTextUpdate = false; + + public AbstractPropertiesFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) { + super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); + mEditor = editor; + } + + protected HashMap getNameToField() { + return mNameToField; + } + + protected ExportEditor getEditor() { + return mEditor; + } + + protected void setInternalTextUpdate(boolean internalTextUpdate) { + mInternalTextUpdate = internalTextUpdate; + } + + protected boolean isInternalTextUpdate() { + return mInternalTextUpdate; + } + + /** + * Adds a modify listener to every text field that will mark the part as dirty. + * + * CONTRACT: Derived classes MUST call this at the end of their constructor. + * + * @see #setFieldModifyListener(Control, ModifyListener) + */ + protected void addModifyListenerToFields() { + ModifyListener markDirtyListener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + // Mark the part as dirty if a field has been changed. + // This will force a commit() operation to store the data in the model. + if (!mInternalTextUpdate) { + markDirty(); + } + } + }; + + for (Control field : mNameToField.values()) { + setFieldModifyListener(field, markDirtyListener); + } + } + + /** + * Sets a listener that will mark the part as dirty when the control is modified. + * The base method only handles {@link Text} fields. + * + * CONTRACT: Derived classes CAN use this to add a listener to their own controls. + * The listener must call {@link #markDirty()} when the control is modified by the user. + * + * @param field A control previously registered with {@link #getNameToField()}. + * @param markDirtyListener A {@link ModifyListener} that invokes {@link #markDirty()}. + * + * @see #isInternalTextUpdate() + */ + protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) { + if (field instanceof Text) { + ((Text) field).addModifyListener(markDirtyListener); + } + } + + /** + * Updates the model based on the content of fields. This is invoked when a field + * has marked the document as dirty. + * + * CONTRACT: Derived classes do not need to override this. + */ + @Override + public void commit(boolean onSave) { + + // We didn't store any information indicating which field was dirty (we could). + // Since there are not many fields, just update all the document lines that + // match our field keywords. + + if (isDirty()) { + mEditor.wrapRewriteSession(new Runnable() { + public void run() { + saveFieldsToModel(); + } + }); + } + + super.commit(onSave); + } + + private void saveFieldsToModel() { + // Get a list of all keywords to process. Go thru the document, replacing in-place + // the ones we can find and remove them from this set. This will leave the list + // of new keywords to add at the end of the document. + HashSet allKeywords = new HashSet(mNameToField.keySet()); + + IDocument doc = mEditor.getDocument(); + int numLines = doc.getNumberOfLines(); + + String delim = null; + try { + delim = numLines > 0 ? doc.getLineDelimiter(0) : null; + } catch (BadLocationException e1) { + // ignore + } + if (delim == null || delim.length() == 0) { + delim = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS ? + "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-2# + } + + for (int i = 0; i < numLines; i++) { + try { + IRegion info = doc.getLineInformation(i); + String line = doc.get(info.getOffset(), info.getLength()); + line = line.trim(); + if (line.startsWith("#")) { //$NON-NLS-1$ + continue; + } + + int pos = line.indexOf('='); + if (pos > 0 && pos < line.length() - 1) { + String key = line.substring(0, pos).trim(); + + Control field = mNameToField.get(key); + if (field != null) { + + // This is the new line to inject + line = key + "=" + getFieldText(field); + + try { + // replace old line by new one. This doesn't change the + // line delimiter. + mInternalTextUpdate = true; + doc.replace(info.getOffset(), info.getLength(), line); + allKeywords.remove(key); + } finally { + mInternalTextUpdate = false; + } + } + } + + } catch (BadLocationException e) { + // TODO log it + AdtPlugin.log(e, "Failed to replace in export.properties"); + } + } + + for (String key : allKeywords) { + Control field = mNameToField.get(key); + if (field != null) { + // This is the new line to inject + String line = key + "=" + getFieldText(field); + + try { + // replace old line by new one + mInternalTextUpdate = true; + + numLines = doc.getNumberOfLines(); + + IRegion info = numLines > 0 ? doc.getLineInformation(numLines - 1) : null; + if (info.getLength() == 0) { + // last line is empty. Insert right before there. + doc.replace(info.getOffset(), info.getLength(), line); + } else { + if (numLines > 0) { + String eofDelim = doc.getLineDelimiter(numLines - 1); + if (eofDelim == null || eofDelim.length() == 0) { + // The document doesn't end with a line delimiter, so add + // one to the line to be written. + line = delim + line; + } + } + + int len = doc.getLength(); + doc.replace(len, 0, line); + } + + allKeywords.remove(key); + } catch (BadLocationException e) { + // TODO log it + AdtPlugin.log(e, "Failed to append to export.properties: %s", line); + } finally { + mInternalTextUpdate = false; + } + } + } + } + + /** + * Used when committing fields values to the model to retrieve the text + * associated with a field. + *

+ * The base method only handles {@link Text} controls. + * + * CONTRACT: Derived classes CAN use this to support their own controls. + * + * @param field A control previously registered with {@link #getNameToField()}. + * @return A non-null string to write to the properties files. + */ + protected String getFieldText(Control field) { + if (field instanceof Text) { + return ((Text) field).getText(); + } + return ""; + } + + /** + * Called after all pages have been created, to let the parts initialize their + * content based on the document's model. + *

+ * The model should be acceded via the {@link ExportEditor}. + * + * @param editor The {@link ExportEditor} instance. + */ + public void onModelInit(ExportEditor editor) { + + // Start with a set of all the possible keywords and remove those we + // found in the document as we read the lines. + HashSet allKeywords = new HashSet(mNameToField.keySet()); + + // Parse the lines in the document for patterns "keyword=value", + // trimming all whitespace and discarding lines that start with # (comments) + // then affect to the internal fields as appropriate. + IDocument doc = editor.getDocument(); + int numLines = doc.getNumberOfLines(); + for (int i = 0; i < numLines; i++) { + try { + IRegion info = doc.getLineInformation(i); + String line = doc.get(info.getOffset(), info.getLength()); + line = line.trim(); + if (line.startsWith("#")) { //$NON-NLS-1$ + continue; + } + + int pos = line.indexOf('='); + if (pos > 0 && pos < line.length() - 1) { + String key = line.substring(0, pos).trim(); + + Control field = mNameToField.get(key); + if (field != null) { + String value = line.substring(pos + 1).trim(); + try { + mInternalTextUpdate = true; + setFieldText(field, value); + allKeywords.remove(key); + } finally { + mInternalTextUpdate = false; + } + } + } + + } catch (BadLocationException e) { + // TODO log it + AdtPlugin.log(e, "Failed to set field to export.properties value"); + } + } + + // Clear the text of any keyword we didn't find in the document + for (String key : allKeywords) { + Control field = mNameToField.get(key); + if (field != null) { + try { + mInternalTextUpdate = true; + setFieldText(field, ""); + allKeywords.remove(key); + } finally { + mInternalTextUpdate = false; + } + } + } + } + + /** + * Used when reading the model to set the field values. + *

+ * The base method only handles {@link Text} controls. + * + * CONTRACT: Derived classes CAN use this to support their own controls. + * + * @param field A control previously registered with {@link #getNameToField()}. + * @param value A non-null string to that was read from the properties files. + * The value is an empty string if the property line is missing. + */ + protected void setFieldText(Control field, String value) { + if (field instanceof Text) { + ((Text) field).setText(value); + } + } + + /** + * Called after the document model has been changed. The model should be acceded via + * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession) + * + * @param editor The {@link ExportEditor} instance. + * @param event Specification of changes applied to document. + */ + public void onModelChanged(ExportEditor editor, DocumentEvent event) { + // To simplify and since we don't have many fields, just reload all the values. + // A better way would to be to look at DocumentEvent which gives us the offset/length + // and text that has changed. + if (!mInternalTextUpdate) { + onModelInit(editor); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java new file mode 100755 index 000000000..50d9fd819 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.editors.AndroidTextEditor; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; + +/** + * Multi-page form editor for export.properties in Export Projects. + */ +public class ExportEditor extends AndroidTextEditor { + + public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".text.ExportEditor"; //$NON-NLS-1$ + + private ExportPropertiesPage mExportPropsPage; + + /** + * Creates the form editor for resources XML files. + */ + public ExportEditor() { + super(); + } + + // ---- Base Class Overrides ---- + + /** + * Returns whether the "save as" operation is supported by this editor. + *

+ * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + /** + * Create the various form pages. + */ + @Override + protected void createFormPages() { + try { + mExportPropsPage = new ExportPropertiesPage(this); + addPage(mExportPropsPage); + } catch (PartInitException e) { + AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ + } + + } + + /* (non-java doc) + * Change the tab/title name to include the project name. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + IFile file = fileInput.getFile(); + setPartName(String.format("%1$s", file.getName())); + } + } + + @Override + protected void postCreatePages() { + super.postCreatePages(); + mExportPropsPage.onModelInit(); + } + + /** + * Indicates changes were made to the document. + * + * @param event Specification of changes applied to document. + */ + @Override + protected void onDocumentChanged(DocumentEvent event) { + super.onDocumentChanged(event); + mExportPropsPage.onModelChanged(event); + } + + // ---- Local Methods ---- + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java new file mode 100755 index 000000000..eff3e4806 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.export; + +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +import java.util.HashMap; + +/** + * Section part for editing the properties in an Export editor. + */ +final class ExportFieldsPart extends AbstractPropertiesFieldsPart { + + public ExportFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) { + super(body, toolkit, editor); + Section section = getSection(); + + section.setText("Export Properties"); + section.setDescription("Properties of export.properties:"); + + Composite table = createTableLayout(toolkit, 2 /* numColumns */); + + createLabel(table, toolkit, + "Available Properties", //label + "List of properties you can edit in export.properties"); //tooltip + + Text packageField = createLabelAndText(table, toolkit, + "Package", //label, + "", //$NON-NLS-1$ value, + "TODO tooltip for Package"); //tooltip + + Text projectsField = createLabelAndText(table, toolkit, + "Projects", //label, + "", //$NON-NLS-1$ value, + "TODO tooltip for Projects"); //tooltip + + Text versionCodeField = createLabelAndText(table, toolkit, + "Version Code", //label, + "", //$NON-NLS-1$ value, + "TODO tooltip for Version Code"); //tooltip + + Text keyStoreField = createLabelAndText(table, toolkit, + "Key Store", //label, + "", //$NON-NLS-1$ value, + "TODO tooltip for Key Store"); //tooltip + + Text keyAliasField = createLabelAndText(table, toolkit, + "Key Alias", //label, + "", //$NON-NLS-1$ value, + "TODO tooltip for Key Alias"); //tooltip + + // Associate each field with the keyword in the properties files. + // TODO there's probably some constant to reuse here. + HashMap map = getNameToField(); + map.put("package", packageField); //$NON-NLS-1$ + map.put("projects", projectsField); //$NON-NLS-1$ + map.put("versionCode", versionCodeField); //$NON-NLS-1$ + map.put("_key.store", keyStoreField); //$NON-NLS-1$ + map.put("_key.alias", keyAliasField); //$NON-NLS-1$ + + addModifyListenerToFields(); + } + + @Override + protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) { + super.setFieldModifyListener(field, markDirtyListener); + // TODO override for custom controls + } + + @Override + protected String getFieldText(Control field) { + // TODO override for custom controls + return super.getFieldText(field); + } + + @Override + protected void setFieldText(Control field, String value) { + // TODO override for custom controls + super.setFieldText(field, value); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java new file mode 100755 index 000000000..31ea988b0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.ui.forms.events.HyperlinkEvent; +import org.eclipse.ui.forms.events.IHyperlinkListener; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Links section part for export properties page. + * Displays some help and some links/actions for the user to use. + */ +final class ExportLinksPart extends ManifestSectionPart { + + private FormText mFormText; + + public ExportLinksPart(Composite body, FormToolkit toolkit, ExportEditor editor) { + super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); + Section section = getSection(); + section.setText("Links"); + section.setDescription("TODO SOME TEXT HERE. You can also edit the XML directly."); + + final Composite table = createTableLayout(toolkit, 2 /* numColumns */); + + StringBuffer buf = new StringBuffer(); + buf.append("

"); // $NON-NLS-1$ + + buf.append("
  • "); + buf.append("TODO Custom Action"); + buf.append(""); //$NON-NLS-1$ + buf.append(": blah blah do something (like build/export)."); + buf.append("
  • "); //$NON-NLS-1$ + + buf.append(String.format("
  • ", // $NON-NLS-1$ + ExportEditor.TEXT_EDITOR_ID)); + buf.append("XML Source"); + buf.append(""); //$NON-NLS-1$ + buf.append(": Directly edit the AndroidManifest.xml file."); + buf.append("
  • "); //$NON-NLS-1$ + + buf.append("
  • "); // $NON-NLS-1$ + buf.append("Documentation: Documentation from the Android SDK for AndroidManifest.xml."); // $NON-NLS-1$ + buf.append("
  • "); //$NON-NLS-1$ + buf.append("
    "); //$NON-NLS-1$ + + mFormText = createFormText(table, toolkit, true, buf.toString(), + false /* setupLayoutData */); + + Image androidLogo = AdtPlugin.getAndroidLogo(); + mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$ + + // Listener for default actions (page change, URL web browser) + mFormText.addHyperlinkListener(editor.createHyperlinkListener()); + + mFormText.addHyperlinkListener(new IHyperlinkListener() { + public void linkExited(HyperlinkEvent e) { + // pass + } + + public void linkEntered(HyperlinkEvent e) { + // pass + } + + public void linkActivated(HyperlinkEvent e) { + String link = e.data.toString(); + if ("action_dosomething".equals(link)) { + MessageBox mb = new MessageBox(table.getShell(), SWT.OK); + mb.setText("Custom Action Invoked"); + mb.open(); + } + } + }); + } + + /** + * Called after all pages have been created, to let the parts initialize their + * content based on the document's model. + *

    + * The model should be acceded via the {@link ExportEditor}. + * + * @param editor The {@link ExportEditor} instance. + */ + public void onModelInit(ExportEditor editor) { + // pass + } + + /** + * Called after the document model has been changed. The model should be acceded via + * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession) + * + * @param editor The {@link ExportEditor} instance. + * @param event Specification of changes applied to document. + */ + public void onModelChanged(ExportEditor editor, DocumentEvent event) { + // pass + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java new file mode 100755 index 000000000..f3db5eea6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.export; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ColumnLayout; +import org.eclipse.ui.forms.widgets.ColumnLayoutData; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.ScrolledForm; + + +/** + * Page for export properties, used by {@link ExportEditor}. + * It displays a part to edit the properties and another part + * to provide some links and actions. + */ +public final class ExportPropertiesPage extends FormPage { + + /** Page id used for switching tabs programmatically */ + final static String PAGE_ID = "export_prop_page"; //$NON-NLS-1$ + + /** Container editor */ + ExportEditor mEditor; + /** Export fields part */ + private ExportFieldsPart mFieldsPart; + /** Export links part */ + private ExportLinksPart mLinksPart; + + public ExportPropertiesPage(ExportEditor editor) { + super(editor, PAGE_ID, "Export Properties"); // tab's label, user visible, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Export Properties"); + form.setImage(AdtPlugin.getAndroidLogo()); + + Composite body = form.getBody(); + FormToolkit toolkit = managedForm.getToolkit(); + + body.setLayout(new ColumnLayout()); + + mFieldsPart = new ExportFieldsPart(body, toolkit, mEditor); + mFieldsPart.getSection().setLayoutData(new ColumnLayoutData()); + managedForm.addPart(mFieldsPart); + + mLinksPart = new ExportLinksPart(body, toolkit, mEditor); + mLinksPart.getSection().setLayoutData(new ColumnLayoutData()); + managedForm.addPart(mLinksPart); + + mFieldsPart.onModelInit(mEditor); + mLinksPart.onModelInit(mEditor); + } + + /** + * Called after all pages have been created, to let the parts initialize their + * content based on the document's model. + *

    + * The model should be acceded via the {@link ExportEditor}. + */ + public void onModelInit() { + if (mFieldsPart != null) { + mFieldsPart.onModelInit(mEditor); + } + + if (mLinksPart != null) { + mLinksPart.onModelInit(mEditor); + } + } + + /** + * Called after the document model has been changed. The model should be acceded via + * the {@link ExportEditor}. + * + * @param event Specification of changes applied to document. + */ + public void onModelChanged(DocumentEvent event) { + if (mFieldsPart != null) { + mFieldsPart.onModelChanged(mEditor, event); + } + + if (mLinksPart != null) { + mLinksPart.onModelChanged(mEditor, event); + } + } +} -- 2.11.0