OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / sdk / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / editors / AndroidTextEditor.java
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
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
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
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.
15  */
16
17 package com.android.ide.eclipse.adt.internal.editors;
18
19 import com.android.ide.eclipse.adt.AdtPlugin;
20
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;
63
64 import java.net.MalformedURLException;
65 import java.net.URL;
66
67 /**
68  * Multi-page form editor for Android text files.
69  * <p/>
70  * It is designed to work with a {@link TextEditor} that will display a text file.
71  * <br/>
72  * Derived classes must implement createFormPages to create the forms before the
73  * source editor. This can be a no-op if desired.
74  */
75 public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener {
76
77     /** Preference name for the current page of this file */
78     private static final String PREF_CURRENT_PAGE = "_current_page";
79
80     /** Id string used to create the Android SDK browser */
81     private static String BROWSER_ID = "android"; // $NON-NLS-1$
82
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$
85
86     /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
87     public static final int TEXT_WIDTH_HINT = 50;
88
89     /** Page index of the text editor (always the last page) */
90     private int mTextPageIndex;
91
92     /** The text editor */
93     private TextEditor mTextEditor;
94
95     /** flag set during page creation */
96     private boolean mIsCreatingPage = false;
97
98     private IDocument mDocument;
99
100     /**
101      * Creates a form editor.
102      */
103     public AndroidTextEditor() {
104         super();
105     }
106
107     // ---- Abstract Methods ----
108
109     /**
110      * Creates the various form pages.
111      * <p/>
112      * Derived classes must implement this to add their own specific tabs.
113      */
114     abstract protected void createFormPages();
115
116     /**
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.
120      */
121     protected void postCreatePages() {
122         // Nothing in the base class.
123     }
124
125     /**
126      * Subclasses should override this method to process the new text model.
127      * This is called after the document has been edited.
128      *
129      * The base implementation is empty.
130      *
131      * @param event Specification of changes applied to document.
132      */
133     protected void onDocumentChanged(DocumentEvent event) {
134         // pass
135     }
136
137     // ---- Base Class Overrides, Interfaces Implemented ----
138
139     /**
140      * Creates the pages of the multi-page editor.
141      */
142     @Override
143     protected void addPages() {
144         createAndroidPages();
145         selectDefaultPage(null /* defaultPageId */);
146     }
147
148     /**
149      * Creates the page for the Android Editors
150      */
151     protected void createAndroidPages() {
152         mIsCreatingPage = true;
153         createFormPages();
154         createTextEditor();
155         createUndoRedoActions();
156         postCreatePages();
157         mIsCreatingPage = false;
158     }
159
160     /**
161      * Returns whether the editor is currently creating its pages.
162      */
163     public boolean isCreatingPages() {
164         return mIsCreatingPage;
165     }
166
167     /**
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.)
171      */
172     private void createUndoRedoActions() {
173         IActionBars bars = getEditorSite().getActionBars();
174         if (bars != null) {
175             IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
176             bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
177
178             action = mTextEditor.getAction(ActionFactory.REDO.getId());
179             bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
180
181             bars.updateActionBars();
182         }
183     }
184
185     /**
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.
189      */
190     protected void selectDefaultPage(String defaultPageId) {
191         if (defaultPageId == null) {
192             if (getEditorInput() instanceof IFileEditorInput) {
193                 IFile file = ((IFileEditorInput) getEditorInput()).getFile();
194
195                 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
196                         getClass().getSimpleName() + PREF_CURRENT_PAGE);
197                 String pageId;
198                 try {
199                     pageId = file.getPersistentProperty(qname);
200                     if (pageId != null) {
201                         defaultPageId = pageId;
202                     }
203                 } catch (CoreException e) {
204                     // ignored
205                 }
206             }
207         }
208
209         if (defaultPageId != null) {
210             try {
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);
218             }
219         }
220     }
221
222     /**
223      * Removes all the pages from the editor.
224      */
225     protected void removePages() {
226         int count = getPageCount();
227         for (int i = count - 1 ; i >= 0 ; i--) {
228             removePage(i);
229         }
230     }
231
232     /**
233      * Overrides the parent's setActivePage to be able to switch to the xml editor.
234      *
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.
239      */
240     @Override
241     public IFormPage setActivePage(String pageId) {
242         if (pageId.equals(TEXT_EDITOR_ID)) {
243             super.setActivePage(mTextPageIndex);
244             return null;
245         } else {
246             return super.setActivePage(pageId);
247         }
248     }
249
250
251     /**
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.
254      *
255      * @see MultiPageEditorPart#pageChange(int)
256      */
257     @Override
258     protected void pageChange(int newPageIndex) {
259         super.pageChange(newPageIndex);
260
261         // Do not record page changes during creation of pages
262         if (mIsCreatingPage) {
263             return;
264         }
265
266         if (getEditorInput() instanceof IFileEditorInput) {
267             IFile file = ((IFileEditorInput) getEditorInput()).getFile();
268
269             QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
270                     getClass().getSimpleName() + PREF_CURRENT_PAGE);
271             try {
272                 file.setPersistentProperty(qname, Integer.toString(newPageIndex));
273             } catch (CoreException e) {
274                 // ignore
275             }
276         }
277     }
278
279     /**
280      * Notifies this listener that some resource changes
281      * are happening, or have already happened.
282      *
283      * Closes all project files on project close.
284      * @see IResourceChangeListener
285      */
286     public void resourceChanged(final IResourceChangeEvent event) {
287         if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
288             Display.getDefault().asyncExec(new Runnable() {
289                 public void run() {
290                     IWorkbenchPage[] pages = getSite().getWorkbenchWindow()
291                             .getPages();
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
297                                     .getEditorInput());
298                             pages[i].closeEditor(editorPart, true);
299                         }
300                     }
301                 }
302             });
303         }
304     }
305
306     /**
307      * Initializes the editor part with a site and input.
308      * <p/>
309      * Checks that the input is an instance of {@link IFileEditorInput}.
310      *
311      * @see FormEditor
312      */
313     @Override
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);
318     }
319
320     /**
321      * Removes attached listeners.
322      *
323      * @see WorkbenchPart
324      */
325     @Override
326     public void dispose() {
327         ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
328
329         super.dispose();
330     }
331
332     /**
333      * Commit all dirty pages then saves the contents of the text editor.
334      * <p/>
335      * This works by committing all data to the XML model and then
336      * asking the Structured XML Editor to save the XML.
337      *
338      * @see IEditorPart
339      */
340     @Override
341     public void doSave(IProgressMonitor monitor) {
342         commitPages(true /* onSave */);
343
344         // The actual "save" operation is done by the Structured XML Editor
345         getEditor(mTextPageIndex).doSave(monitor);
346     }
347
348     /* (non-Javadoc)
349      * Saves the contents of this editor to another object.
350      * <p>
351      * Subclasses must override this method to implement the open-save-close lifecycle
352      * for an editor.  For greater details, see <code>IEditorPart</code>
353      * </p>
354      *
355      * @see IEditorPart
356      */
357     @Override
358     public void doSaveAs() {
359         commitPages(true /* onSave */);
360
361         IEditorPart editor = getEditor(mTextPageIndex);
362         editor.doSaveAs();
363         setPageText(mTextPageIndex, editor.getTitle());
364         setInput(editor.getEditorInput());
365     }
366
367     /**
368      * Commits all dirty pages in the editor. This method should
369      * be called as a first step of a 'save' operation.
370      * <p/>
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.
375      * <p/>
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.
379      *
380      * @param onSave <code>true</code> if commit is performed as part
381      * of the 'save' operation, <code>false</code> otherwise.
382      * @since 3.3
383      */
384     @Override
385     public void commitPages(boolean onSave) {
386         if (pages != null) {
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);
394                     }
395                 }
396             }
397         }
398     }
399
400     /* (non-Javadoc)
401      * Returns whether the "save as" operation is supported by this editor.
402      * <p>
403      * Subclasses must override this method to implement the open-save-close lifecycle
404      * for an editor.  For greater details, see <code>IEditorPart</code>
405      * </p>
406      *
407      * @see IEditorPart
408      */
409     @Override
410     public boolean isSaveAsAllowed() {
411         return false;
412     }
413
414     // ---- Local methods ----
415
416
417     /**
418      * Helper method that creates a new hyper-link Listener.
419      * Used by derived classes which need active links in {@link FormText}.
420      * <p/>
421      * This link listener handles two kinds of URLs:
422      * <ul>
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.
427      * </ul>
428      *
429      * @return A new hyper-link listener for FormText to use.
430      */
431     public final IHyperlinkListener createHyperlinkListener() {
432         return new HyperlinkAdapter() {
433             /**
434              * Switch to the page corresponding to the link that has just been clicked.
435              * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
436              */
437             @Override
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:") */));
447                 }
448             }
449         };
450     }
451
452     /**
453      * Open the http link into a browser
454      *
455      * @param link The URL to open in a browser
456      */
457     private void openLinkInBrowser(String link) {
458         try {
459             IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
460             wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
461         } catch (PartInitException e1) {
462             // pass
463         } catch (MalformedURLException e1) {
464             // pass
465         }
466     }
467
468     /**
469      * Creates the XML source editor.
470      * <p/>
471      * Memorizes the index page of the source editor (it's always the last page, but the number
472      * of pages before can change.)
473      * <br/>
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.
477      * <p/>
478      * Called only once <em>after</em> createFormPages.
479      */
480     private void createTextEditor() {
481         try {
482             mTextEditor = new TextEditor();
483             int index = addPage(mTextEditor, getEditorInput());
484             mTextPageIndex = index;
485             setPageText(index, mTextEditor.getTitle());
486
487             IDocumentProvider provider = mTextEditor.getDocumentProvider();
488             mDocument = provider.getDocument(getEditorInput());
489
490             mDocument.addDocumentListener(new IDocumentListener() {
491                 public void documentChanged(DocumentEvent event) {
492                     onDocumentChanged(event);
493                 }
494
495                 public void documentAboutToBeChanged(DocumentEvent event) {
496                     // ignore
497                 }
498             });
499
500
501         } catch (PartInitException e) {
502             ErrorDialog.openError(getSite().getShell(),
503                     "Android Text Editor Error", null, e.getStatus());
504         }
505     }
506
507     /**
508      * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
509      * the current file input.
510      * <p/>
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.
515      */
516     public IDocument getDocument() {
517         return mDocument;
518     }
519
520     /**
521      * Returns the {@link IProject} for the edited file.
522      */
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();
529
530                 if (inputFile != null) {
531                     return inputFile.getProject();
532                 }
533             }
534         }
535
536         return null;
537     }
538
539     /**
540      * Runs the given operation in the context of a document RewriteSession.
541      * Takes care of properly starting and stopping the operation.
542      * <p/>
543      * The operation itself should just access {@link #getDocument()} and use the
544      * normal document's API to manipulate it.
545      *
546      * @see #getDocument()
547      */
548     public void wrapRewriteSession(Runnable operation) {
549         if (mDocument instanceof IDocumentExtension4) {
550             IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
551
552             DocumentRewriteSession session = null;
553             try {
554                 session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
555
556                 operation.run();
557             } catch(IllegalStateException e) {
558                 AdtPlugin.log(e, "wrapRewriteSession failed");
559                 e.printStackTrace();
560             } finally {
561                 if (session != null) {
562                     doc4.stopRewriteSession(session);
563                 }
564             }
565
566         } else {
567             // Not an IDocumentExtension4? Unlikely. Try the operation anyway.
568             operation.run();
569         }
570     }
571
572 }