2 * Copyright (C) 2010 The Android Open Source Project
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.eclipse.org/org/documents/epl-v10.php
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.ide.eclipse.adt.internal.editors;
19 import com.android.ide.eclipse.adt.AdtPlugin;
21 import org.eclipse.core.internal.filebuffers.SynchronizableDocument;
22 import org.eclipse.core.resources.IFile;
23 import org.eclipse.core.resources.IProject;
24 import org.eclipse.core.resources.IResource;
25 import org.eclipse.core.resources.IResourceChangeEvent;
26 import org.eclipse.core.resources.IResourceChangeListener;
27 import org.eclipse.core.resources.ResourcesPlugin;
28 import org.eclipse.core.runtime.CoreException;
29 import org.eclipse.core.runtime.IProgressMonitor;
30 import org.eclipse.core.runtime.QualifiedName;
31 import org.eclipse.jface.action.IAction;
32 import org.eclipse.jface.dialogs.ErrorDialog;
33 import org.eclipse.jface.text.DocumentEvent;
34 import org.eclipse.jface.text.DocumentRewriteSession;
35 import org.eclipse.jface.text.DocumentRewriteSessionType;
36 import org.eclipse.jface.text.IDocument;
37 import org.eclipse.jface.text.IDocumentExtension4;
38 import org.eclipse.jface.text.IDocumentListener;
39 import org.eclipse.swt.widgets.Display;
40 import org.eclipse.ui.IActionBars;
41 import org.eclipse.ui.IEditorInput;
42 import org.eclipse.ui.IEditorPart;
43 import org.eclipse.ui.IEditorSite;
44 import org.eclipse.ui.IFileEditorInput;
45 import org.eclipse.ui.IWorkbenchPage;
46 import org.eclipse.ui.PartInitException;
47 import org.eclipse.ui.actions.ActionFactory;
48 import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
49 import org.eclipse.ui.editors.text.TextEditor;
50 import org.eclipse.ui.forms.IManagedForm;
51 import org.eclipse.ui.forms.editor.FormEditor;
52 import org.eclipse.ui.forms.editor.IFormPage;
53 import org.eclipse.ui.forms.events.HyperlinkAdapter;
54 import org.eclipse.ui.forms.events.HyperlinkEvent;
55 import org.eclipse.ui.forms.events.IHyperlinkListener;
56 import org.eclipse.ui.forms.widgets.FormText;
57 import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
58 import org.eclipse.ui.part.FileEditorInput;
59 import org.eclipse.ui.part.MultiPageEditorPart;
60 import org.eclipse.ui.part.WorkbenchPart;
61 import org.eclipse.ui.texteditor.IDocumentProvider;
62 import org.eclipse.wst.sse.ui.StructuredTextEditor;
64 import java.net.MalformedURLException;
68 * Multi-page form editor for Android text files.
70 * It is designed to work with a {@link TextEditor} that will display a text file.
72 * Derived classes must implement createFormPages to create the forms before the
73 * source editor. This can be a no-op if desired.
75 public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener {
77 /** Preference name for the current page of this file */
78 private static final String PREF_CURRENT_PAGE = "_current_page";
80 /** Id string used to create the Android SDK browser */
81 private static String BROWSER_ID = "android"; // $NON-NLS-1$
83 /** Page id of the XML source editor, used for switching tabs programmatically */
84 public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
86 /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
87 public static final int TEXT_WIDTH_HINT = 50;
89 /** Page index of the text editor (always the last page) */
90 private int mTextPageIndex;
92 /** The text editor */
93 private TextEditor mTextEditor;
95 /** flag set during page creation */
96 private boolean mIsCreatingPage = false;
98 private IDocument mDocument;
101 * Creates a form editor.
103 public AndroidTextEditor() {
107 // ---- Abstract Methods ----
110 * Creates the various form pages.
112 * Derived classes must implement this to add their own specific tabs.
114 abstract protected void createFormPages();
117 * Called by the base class {@link AndroidTextEditor} once all pages (custom form pages
118 * as well as text editor page) have been created. This give a chance to deriving
119 * classes to adjust behavior once the text page has been created.
121 protected void postCreatePages() {
122 // Nothing in the base class.
126 * Subclasses should override this method to process the new text model.
127 * This is called after the document has been edited.
129 * The base implementation is empty.
131 * @param event Specification of changes applied to document.
133 protected void onDocumentChanged(DocumentEvent event) {
137 // ---- Base Class Overrides, Interfaces Implemented ----
140 * Creates the pages of the multi-page editor.
143 protected void addPages() {
144 createAndroidPages();
145 selectDefaultPage(null /* defaultPageId */);
149 * Creates the page for the Android Editors
151 protected void createAndroidPages() {
152 mIsCreatingPage = true;
155 createUndoRedoActions();
157 mIsCreatingPage = false;
161 * Returns whether the editor is currently creating its pages.
163 public boolean isCreatingPages() {
164 return mIsCreatingPage;
168 * Creates undo redo actions for the editor site (so that it works for any page of this
169 * multi-page editor) by re-using the actions defined by the {@link TextEditor}
170 * (aka the XML text editor.)
172 private void createUndoRedoActions() {
173 IActionBars bars = getEditorSite().getActionBars();
175 IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
176 bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
178 action = mTextEditor.getAction(ActionFactory.REDO.getId());
179 bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
181 bars.updateActionBars();
186 * Selects the default active page.
187 * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
188 * find the default page in the properties of the {@link IResource} object being edited.
190 protected void selectDefaultPage(String defaultPageId) {
191 if (defaultPageId == null) {
192 if (getEditorInput() instanceof IFileEditorInput) {
193 IFile file = ((IFileEditorInput) getEditorInput()).getFile();
195 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
196 getClass().getSimpleName() + PREF_CURRENT_PAGE);
199 pageId = file.getPersistentProperty(qname);
200 if (pageId != null) {
201 defaultPageId = pageId;
203 } catch (CoreException e) {
209 if (defaultPageId != null) {
211 setActivePage(Integer.parseInt(defaultPageId));
212 } catch (Exception e) {
213 // We can get NumberFormatException from parseInt but also
214 // AssertionError from setActivePage when the index is out of bounds.
215 // Generally speaking we just want to ignore any exception and fall back on the
216 // first page rather than crash the editor load. Logging the error is enough.
217 AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
223 * Removes all the pages from the editor.
225 protected void removePages() {
226 int count = getPageCount();
227 for (int i = count - 1 ; i >= 0 ; i--) {
233 * Overrides the parent's setActivePage to be able to switch to the xml editor.
235 * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
236 * This is needed because the editor doesn't actually derive from IFormPage and thus
237 * doesn't have the get-by-page-id method. In this case, the method returns null since
238 * IEditorPart does not implement IFormPage.
241 public IFormPage setActivePage(String pageId) {
242 if (pageId.equals(TEXT_EDITOR_ID)) {
243 super.setActivePage(mTextPageIndex);
246 return super.setActivePage(pageId);
252 * Notifies this multi-page editor that the page with the given id has been
253 * activated. This method is called when the user selects a different tab.
255 * @see MultiPageEditorPart#pageChange(int)
258 protected void pageChange(int newPageIndex) {
259 super.pageChange(newPageIndex);
261 // Do not record page changes during creation of pages
262 if (mIsCreatingPage) {
266 if (getEditorInput() instanceof IFileEditorInput) {
267 IFile file = ((IFileEditorInput) getEditorInput()).getFile();
269 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
270 getClass().getSimpleName() + PREF_CURRENT_PAGE);
272 file.setPersistentProperty(qname, Integer.toString(newPageIndex));
273 } catch (CoreException e) {
280 * Notifies this listener that some resource changes
281 * are happening, or have already happened.
283 * Closes all project files on project close.
284 * @see IResourceChangeListener
286 public void resourceChanged(final IResourceChangeEvent event) {
287 if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
288 Display.getDefault().asyncExec(new Runnable() {
290 IWorkbenchPage[] pages = getSite().getWorkbenchWindow()
292 for (int i = 0; i < pages.length; i++) {
293 if (((FileEditorInput)mTextEditor.getEditorInput())
294 .getFile().getProject().equals(
295 event.getResource())) {
296 IEditorPart editorPart = pages[i].findEditor(mTextEditor
298 pages[i].closeEditor(editorPart, true);
307 * Initializes the editor part with a site and input.
309 * Checks that the input is an instance of {@link IFileEditorInput}.
314 public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
315 if (!(editorInput instanceof IFileEditorInput))
316 throw new PartInitException("Invalid Input: Must be IFileEditorInput");
317 super.init(site, editorInput);
321 * Removes attached listeners.
326 public void dispose() {
327 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
333 * Commit all dirty pages then saves the contents of the text editor.
335 * This works by committing all data to the XML model and then
336 * asking the Structured XML Editor to save the XML.
341 public void doSave(IProgressMonitor monitor) {
342 commitPages(true /* onSave */);
344 // The actual "save" operation is done by the Structured XML Editor
345 getEditor(mTextPageIndex).doSave(monitor);
349 * Saves the contents of this editor to another object.
351 * Subclasses must override this method to implement the open-save-close lifecycle
352 * for an editor. For greater details, see <code>IEditorPart</code>
358 public void doSaveAs() {
359 commitPages(true /* onSave */);
361 IEditorPart editor = getEditor(mTextPageIndex);
363 setPageText(mTextPageIndex, editor.getTitle());
364 setInput(editor.getEditorInput());
368 * Commits all dirty pages in the editor. This method should
369 * be called as a first step of a 'save' operation.
371 * This is the same implementation as in {@link FormEditor}
372 * except it fixes two bugs: a cast to IFormPage is done
373 * from page.get(i) <em>before</em> being tested with instanceof.
374 * Another bug is that the last page might be a null pointer.
376 * The incorrect casting makes the original implementation crash due
377 * to our {@link StructuredTextEditor} not being an {@link IFormPage}
378 * so we have to override and duplicate to fix it.
380 * @param onSave <code>true</code> if commit is performed as part
381 * of the 'save' operation, <code>false</code> otherwise.
385 public void commitPages(boolean onSave) {
387 for (int i = 0; i < pages.size(); i++) {
388 Object page = pages.get(i);
389 if (page != null && page instanceof IFormPage) {
390 IFormPage form_page = (IFormPage) page;
391 IManagedForm managed_form = form_page.getManagedForm();
392 if (managed_form != null && managed_form.isDirty()) {
393 managed_form.commit(onSave);
401 * Returns whether the "save as" operation is supported by this editor.
403 * Subclasses must override this method to implement the open-save-close lifecycle
404 * for an editor. For greater details, see <code>IEditorPart</code>
410 public boolean isSaveAsAllowed() {
414 // ---- Local methods ----
418 * Helper method that creates a new hyper-link Listener.
419 * Used by derived classes which need active links in {@link FormText}.
421 * This link listener handles two kinds of URLs:
423 * <li> Links starting with "http" are simply sent to a local browser.
424 * <li> Links starting with "file:/" are simply sent to a local browser.
425 * <li> Links starting with "page:" are expected to be an editor page id to switch to.
426 * <li> Other links are ignored.
429 * @return A new hyper-link listener for FormText to use.
431 public final IHyperlinkListener createHyperlinkListener() {
432 return new HyperlinkAdapter() {
434 * Switch to the page corresponding to the link that has just been clicked.
435 * For this purpose, the HREF of the <a> tags above is the page ID to switch to.
438 public void linkActivated(HyperlinkEvent e) {
439 super.linkActivated(e);
440 String link = e.data.toString();
441 if (link.startsWith("http") || //$NON-NLS-1$
442 link.startsWith("file:/")) { //$NON-NLS-1$
443 openLinkInBrowser(link);
444 } else if (link.startsWith("page:")) { //$NON-NLS-1$
445 // Switch to an internal page
446 setActivePage(link.substring(5 /* strlen("page:") */));
453 * Open the http link into a browser
455 * @param link The URL to open in a browser
457 private void openLinkInBrowser(String link) {
459 IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
460 wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
461 } catch (PartInitException e1) {
463 } catch (MalformedURLException e1) {
469 * Creates the XML source editor.
471 * Memorizes the index page of the source editor (it's always the last page, but the number
472 * of pages before can change.)
474 * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
475 * Finally triggers modelChanged() on the model listener -- derived classes can use this
476 * to initialize the model the first time.
478 * Called only once <em>after</em> createFormPages.
480 private void createTextEditor() {
482 mTextEditor = new TextEditor();
483 int index = addPage(mTextEditor, getEditorInput());
484 mTextPageIndex = index;
485 setPageText(index, mTextEditor.getTitle());
487 IDocumentProvider provider = mTextEditor.getDocumentProvider();
488 mDocument = provider.getDocument(getEditorInput());
490 mDocument.addDocumentListener(new IDocumentListener() {
491 public void documentChanged(DocumentEvent event) {
492 onDocumentChanged(event);
495 public void documentAboutToBeChanged(DocumentEvent event) {
501 } catch (PartInitException e) {
502 ErrorDialog.openError(getSite().getShell(),
503 "Android Text Editor Error", null, e.getStatus());
508 * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
509 * the current file input.
511 * All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}.
512 * The actual document instance is a {@link SynchronizableDocument}, which creates a lock
513 * around read/set operations. The base API provided by {@link IDocument} provides ways to
514 * manipulate the document line per line or as a bulk.
516 public IDocument getDocument() {
521 * Returns the {@link IProject} for the edited file.
523 public IProject getProject() {
524 if (mTextEditor != null) {
525 IEditorInput input = mTextEditor.getEditorInput();
526 if (input instanceof FileEditorInput) {
527 FileEditorInput fileInput = (FileEditorInput)input;
528 IFile inputFile = fileInput.getFile();
530 if (inputFile != null) {
531 return inputFile.getProject();
540 * Runs the given operation in the context of a document RewriteSession.
541 * Takes care of properly starting and stopping the operation.
543 * The operation itself should just access {@link #getDocument()} and use the
544 * normal document's API to manipulate it.
546 * @see #getDocument()
548 public void wrapRewriteSession(Runnable operation) {
549 if (mDocument instanceof IDocumentExtension4) {
550 IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
552 DocumentRewriteSession session = null;
554 session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
557 } catch(IllegalStateException e) {
558 AdtPlugin.log(e, "wrapRewriteSession failed");
561 if (session != null) {
562 doc4.stopRewriteSession(session);
567 // Not an IDocumentExtension4? Unlikely. Try the operation anyway.