Home | History | Annotate | Download | only in editors
      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      * Returns the {@link IFile} matching the editor's input or null.
    322      * <p/>
    323      * By construction, the editor input has to be an {@link IFileEditorInput} so it must
    324      * have an associated {@link IFile}. Null can only be returned if this editor has no
    325      * input somehow.
    326      */
    327     public IFile getFile() {
    328         if (getEditorInput() instanceof IFileEditorInput) {
    329             return ((IFileEditorInput) getEditorInput()).getFile();
    330         }
    331         return null;
    332     }
    333 
    334     /**
    335      * Removes attached listeners.
    336      *
    337      * @see WorkbenchPart
    338      */
    339     @Override
    340     public void dispose() {
    341         ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    342 
    343         super.dispose();
    344     }
    345 
    346     /**
    347      * Commit all dirty pages then saves the contents of the text editor.
    348      * <p/>
    349      * This works by committing all data to the XML model and then
    350      * asking the Structured XML Editor to save the XML.
    351      *
    352      * @see IEditorPart
    353      */
    354     @Override
    355     public void doSave(IProgressMonitor monitor) {
    356         commitPages(true /* onSave */);
    357 
    358         // The actual "save" operation is done by the Structured XML Editor
    359         getEditor(mTextPageIndex).doSave(monitor);
    360     }
    361 
    362     /* (non-Javadoc)
    363      * Saves the contents of this editor to another object.
    364      * <p>
    365      * Subclasses must override this method to implement the open-save-close lifecycle
    366      * for an editor.  For greater details, see <code>IEditorPart</code>
    367      * </p>
    368      *
    369      * @see IEditorPart
    370      */
    371     @Override
    372     public void doSaveAs() {
    373         commitPages(true /* onSave */);
    374 
    375         IEditorPart editor = getEditor(mTextPageIndex);
    376         editor.doSaveAs();
    377         setPageText(mTextPageIndex, editor.getTitle());
    378         setInput(editor.getEditorInput());
    379     }
    380 
    381     /**
    382      * Commits all dirty pages in the editor. This method should
    383      * be called as a first step of a 'save' operation.
    384      * <p/>
    385      * This is the same implementation as in {@link FormEditor}
    386      * except it fixes two bugs: a cast to IFormPage is done
    387      * from page.get(i) <em>before</em> being tested with instanceof.
    388      * Another bug is that the last page might be a null pointer.
    389      * <p/>
    390      * The incorrect casting makes the original implementation crash due
    391      * to our {@link StructuredTextEditor} not being an {@link IFormPage}
    392      * so we have to override and duplicate to fix it.
    393      *
    394      * @param onSave <code>true</code> if commit is performed as part
    395      * of the 'save' operation, <code>false</code> otherwise.
    396      * @since 3.3
    397      */
    398     @Override
    399     public void commitPages(boolean onSave) {
    400         if (pages != null) {
    401             for (int i = 0; i < pages.size(); i++) {
    402                 Object page = pages.get(i);
    403                 if (page != null && page instanceof IFormPage) {
    404                     IFormPage form_page = (IFormPage) page;
    405                     IManagedForm managed_form = form_page.getManagedForm();
    406                     if (managed_form != null && managed_form.isDirty()) {
    407                         managed_form.commit(onSave);
    408                     }
    409                 }
    410             }
    411         }
    412     }
    413 
    414     /* (non-Javadoc)
    415      * Returns whether the "save as" operation is supported by this editor.
    416      * <p>
    417      * Subclasses must override this method to implement the open-save-close lifecycle
    418      * for an editor.  For greater details, see <code>IEditorPart</code>
    419      * </p>
    420      *
    421      * @see IEditorPart
    422      */
    423     @Override
    424     public boolean isSaveAsAllowed() {
    425         return false;
    426     }
    427 
    428     // ---- Local methods ----
    429 
    430 
    431     /**
    432      * Helper method that creates a new hyper-link Listener.
    433      * Used by derived classes which need active links in {@link FormText}.
    434      * <p/>
    435      * This link listener handles two kinds of URLs:
    436      * <ul>
    437      * <li> Links starting with "http" are simply sent to a local browser.
    438      * <li> Links starting with "file:/" are simply sent to a local browser.
    439      * <li> Links starting with "page:" are expected to be an editor page id to switch to.
    440      * <li> Other links are ignored.
    441      * </ul>
    442      *
    443      * @return A new hyper-link listener for FormText to use.
    444      */
    445     public final IHyperlinkListener createHyperlinkListener() {
    446         return new HyperlinkAdapter() {
    447             /**
    448              * Switch to the page corresponding to the link that has just been clicked.
    449              * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
    450              */
    451             @Override
    452             public void linkActivated(HyperlinkEvent e) {
    453                 super.linkActivated(e);
    454                 String link = e.data.toString();
    455                 if (link.startsWith("http") ||          //$NON-NLS-1$
    456                         link.startsWith("file:/")) {    //$NON-NLS-1$
    457                     openLinkInBrowser(link);
    458                 } else if (link.startsWith("page:")) {  //$NON-NLS-1$
    459                     // Switch to an internal page
    460                     setActivePage(link.substring(5 /* strlen("page:") */));
    461                 }
    462             }
    463         };
    464     }
    465 
    466     /**
    467      * Open the http link into a browser
    468      *
    469      * @param link The URL to open in a browser
    470      */
    471     private void openLinkInBrowser(String link) {
    472         try {
    473             IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
    474             wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
    475         } catch (PartInitException e1) {
    476             // pass
    477         } catch (MalformedURLException e1) {
    478             // pass
    479         }
    480     }
    481 
    482     /**
    483      * Creates the XML source editor.
    484      * <p/>
    485      * Memorizes the index page of the source editor (it's always the last page, but the number
    486      * of pages before can change.)
    487      * <br/>
    488      * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
    489      * Finally triggers modelChanged() on the model listener -- derived classes can use this
    490      * to initialize the model the first time.
    491      * <p/>
    492      * Called only once <em>after</em> createFormPages.
    493      */
    494     private void createTextEditor() {
    495         try {
    496             mTextEditor = new TextEditor();
    497             int index = addPage(mTextEditor, getEditorInput());
    498             mTextPageIndex = index;
    499             setPageText(index, mTextEditor.getTitle());
    500 
    501             IDocumentProvider provider = mTextEditor.getDocumentProvider();
    502             mDocument = provider.getDocument(getEditorInput());
    503 
    504             mDocument.addDocumentListener(new IDocumentListener() {
    505                 public void documentChanged(DocumentEvent event) {
    506                     onDocumentChanged(event);
    507                 }
    508 
    509                 public void documentAboutToBeChanged(DocumentEvent event) {
    510                     // ignore
    511                 }
    512             });
    513 
    514 
    515         } catch (PartInitException e) {
    516             ErrorDialog.openError(getSite().getShell(),
    517                     "Android Text Editor Error", null, e.getStatus());
    518         }
    519     }
    520 
    521     /**
    522      * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
    523      * the current file input.
    524      * <p/>
    525      * All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}.
    526      * The actual document instance is a {@link SynchronizableDocument}, which creates a lock
    527      * around read/set operations. The base API provided by {@link IDocument} provides ways to
    528      * manipulate the document line per line or as a bulk.
    529      */
    530     public IDocument getDocument() {
    531         return mDocument;
    532     }
    533 
    534     /**
    535      * Returns the {@link IProject} for the edited file.
    536      */
    537     public IProject getProject() {
    538         if (mTextEditor != null) {
    539             IEditorInput input = mTextEditor.getEditorInput();
    540             if (input instanceof FileEditorInput) {
    541                 FileEditorInput fileInput = (FileEditorInput)input;
    542                 IFile inputFile = fileInput.getFile();
    543 
    544                 if (inputFile != null) {
    545                     return inputFile.getProject();
    546                 }
    547             }
    548         }
    549 
    550         return null;
    551     }
    552 
    553     /**
    554      * Runs the given operation in the context of a document RewriteSession.
    555      * Takes care of properly starting and stopping the operation.
    556      * <p/>
    557      * The operation itself should just access {@link #getDocument()} and use the
    558      * normal document's API to manipulate it.
    559      *
    560      * @see #getDocument()
    561      */
    562     public void wrapRewriteSession(Runnable operation) {
    563         if (mDocument instanceof IDocumentExtension4) {
    564             IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
    565 
    566             DocumentRewriteSession session = null;
    567             try {
    568                 session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
    569 
    570                 operation.run();
    571             } catch(IllegalStateException e) {
    572                 AdtPlugin.log(e, "wrapRewriteSession failed");
    573                 e.printStackTrace();
    574             } finally {
    575                 if (session != null) {
    576                     doc4.stopRewriteSession(session);
    577                 }
    578             }
    579 
    580         } else {
    581             // Not an IDocumentExtension4? Unlikely. Try the operation anyway.
    582             operation.run();
    583         }
    584     }
    585 
    586 }
    587