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 @SuppressWarnings("restriction")
     76 public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener {
     77 
     78     /** Preference name for the current page of this file */
     79     private static final String PREF_CURRENT_PAGE = "_current_page";
     80 
     81     /** Id string used to create the Android SDK browser */
     82     private static String BROWSER_ID = "android"; //$NON-NLS-1$
     83 
     84     /** Page id of the XML source editor, used for switching tabs programmatically */
     85     public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
     86 
     87     /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
     88     public static final int TEXT_WIDTH_HINT = 50;
     89 
     90     /** Page index of the text editor (always the last page) */
     91     private int mTextPageIndex;
     92 
     93     /** The text editor */
     94     private TextEditor mTextEditor;
     95 
     96     /** flag set during page creation */
     97     private boolean mIsCreatingPage = false;
     98 
     99     private IDocument mDocument;
    100 
    101     /**
    102      * Creates a form editor.
    103      */
    104     public AndroidTextEditor() {
    105         super();
    106     }
    107 
    108     // ---- Abstract Methods ----
    109 
    110     /**
    111      * Creates the various form pages.
    112      * <p/>
    113      * Derived classes must implement this to add their own specific tabs.
    114      */
    115     abstract protected void createFormPages();
    116 
    117     /**
    118      * Called by the base class {@link AndroidTextEditor} once all pages (custom form pages
    119      * as well as text editor page) have been created. This give a chance to deriving
    120      * classes to adjust behavior once the text page has been created.
    121      */
    122     protected void postCreatePages() {
    123         // Nothing in the base class.
    124     }
    125 
    126     /**
    127      * Subclasses should override this method to process the new text model.
    128      * This is called after the document has been edited.
    129      *
    130      * The base implementation is empty.
    131      *
    132      * @param event Specification of changes applied to document.
    133      */
    134     protected void onDocumentChanged(DocumentEvent event) {
    135         // pass
    136     }
    137 
    138     // ---- Base Class Overrides, Interfaces Implemented ----
    139 
    140     /**
    141      * Creates the pages of the multi-page editor.
    142      */
    143     @Override
    144     protected void addPages() {
    145         createAndroidPages();
    146         selectDefaultPage(null /* defaultPageId */);
    147     }
    148 
    149     /**
    150      * Creates the page for the Android Editors
    151      */
    152     protected void createAndroidPages() {
    153         mIsCreatingPage = true;
    154         createFormPages();
    155         createTextEditor();
    156         createUndoRedoActions();
    157         postCreatePages();
    158         mIsCreatingPage = false;
    159     }
    160 
    161     /**
    162      * Returns whether the editor is currently creating its pages.
    163      */
    164     public boolean isCreatingPages() {
    165         return mIsCreatingPage;
    166     }
    167 
    168     /**
    169      * Creates undo redo actions for the editor site (so that it works for any page of this
    170      * multi-page editor) by re-using the actions defined by the {@link TextEditor}
    171      * (aka the XML text editor.)
    172      */
    173     private void createUndoRedoActions() {
    174         IActionBars bars = getEditorSite().getActionBars();
    175         if (bars != null) {
    176             IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
    177             bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
    178 
    179             action = mTextEditor.getAction(ActionFactory.REDO.getId());
    180             bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
    181 
    182             bars.updateActionBars();
    183         }
    184     }
    185 
    186     /**
    187      * Selects the default active page.
    188      * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
    189      * find the default page in the properties of the {@link IResource} object being edited.
    190      */
    191     protected void selectDefaultPage(String defaultPageId) {
    192         if (defaultPageId == null) {
    193             if (getEditorInput() instanceof IFileEditorInput) {
    194                 IFile file = ((IFileEditorInput) getEditorInput()).getFile();
    195 
    196                 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
    197                         getClass().getSimpleName() + PREF_CURRENT_PAGE);
    198                 String pageId;
    199                 try {
    200                     pageId = file.getPersistentProperty(qname);
    201                     if (pageId != null) {
    202                         defaultPageId = pageId;
    203                     }
    204                 } catch (CoreException e) {
    205                     // ignored
    206                 }
    207             }
    208         }
    209 
    210         if (defaultPageId != null) {
    211             try {
    212                 setActivePage(Integer.parseInt(defaultPageId));
    213             } catch (Exception e) {
    214                 // We can get NumberFormatException from parseInt but also
    215                 // AssertionError from setActivePage when the index is out of bounds.
    216                 // Generally speaking we just want to ignore any exception and fall back on the
    217                 // first page rather than crash the editor load. Logging the error is enough.
    218                 AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
    219             }
    220         }
    221     }
    222 
    223     /**
    224      * Removes all the pages from the editor.
    225      */
    226     protected void removePages() {
    227         int count = getPageCount();
    228         for (int i = count - 1 ; i >= 0 ; i--) {
    229             removePage(i);
    230         }
    231     }
    232 
    233     /**
    234      * Overrides the parent's setActivePage to be able to switch to the xml editor.
    235      *
    236      * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
    237      * This is needed because the editor doesn't actually derive from IFormPage and thus
    238      * doesn't have the get-by-page-id method. In this case, the method returns null since
    239      * IEditorPart does not implement IFormPage.
    240      */
    241     @Override
    242     public IFormPage setActivePage(String pageId) {
    243         if (pageId.equals(TEXT_EDITOR_ID)) {
    244             super.setActivePage(mTextPageIndex);
    245             return null;
    246         } else {
    247             return super.setActivePage(pageId);
    248         }
    249     }
    250 
    251 
    252     /**
    253      * Notifies this multi-page editor that the page with the given id has been
    254      * activated. This method is called when the user selects a different tab.
    255      *
    256      * @see MultiPageEditorPart#pageChange(int)
    257      */
    258     @Override
    259     protected void pageChange(int newPageIndex) {
    260         super.pageChange(newPageIndex);
    261 
    262         // Do not record page changes during creation of pages
    263         if (mIsCreatingPage) {
    264             return;
    265         }
    266 
    267         if (getEditorInput() instanceof IFileEditorInput) {
    268             IFile file = ((IFileEditorInput) getEditorInput()).getFile();
    269 
    270             QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
    271                     getClass().getSimpleName() + PREF_CURRENT_PAGE);
    272             try {
    273                 file.setPersistentProperty(qname, Integer.toString(newPageIndex));
    274             } catch (CoreException e) {
    275                 // ignore
    276             }
    277         }
    278     }
    279 
    280     /**
    281      * Notifies this listener that some resource changes
    282      * are happening, or have already happened.
    283      *
    284      * Closes all project files on project close.
    285      * @see IResourceChangeListener
    286      */
    287     @Override
    288     public void resourceChanged(final IResourceChangeEvent event) {
    289         if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
    290             Display.getDefault().asyncExec(new Runnable() {
    291                 @Override
    292                 public void run() {
    293                     @SuppressWarnings("hiding")
    294                     IWorkbenchPage[] pages = getSite().getWorkbenchWindow().getPages();
    295                     for (int i = 0; i < pages.length; i++) {
    296                         if (((FileEditorInput)mTextEditor.getEditorInput())
    297                                 .getFile().getProject().equals(
    298                                         event.getResource())) {
    299                             IEditorPart editorPart = pages[i].findEditor(mTextEditor
    300                                     .getEditorInput());
    301                             pages[i].closeEditor(editorPart, true);
    302                         }
    303                     }
    304                 }
    305             });
    306         }
    307     }
    308 
    309     /**
    310      * Initializes the editor part with a site and input.
    311      * <p/>
    312      * Checks that the input is an instance of {@link IFileEditorInput}.
    313      *
    314      * @see FormEditor
    315      */
    316     @Override
    317     public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
    318         if (!(editorInput instanceof IFileEditorInput))
    319             throw new PartInitException("Invalid Input: Must be IFileEditorInput");
    320         super.init(site, editorInput);
    321     }
    322 
    323     /**
    324      * Returns the {@link IFile} matching the editor's input or null.
    325      * <p/>
    326      * By construction, the editor input has to be an {@link IFileEditorInput} so it must
    327      * have an associated {@link IFile}. Null can only be returned if this editor has no
    328      * input somehow.
    329      */
    330     public IFile getFile() {
    331         if (getEditorInput() instanceof IFileEditorInput) {
    332             return ((IFileEditorInput) getEditorInput()).getFile();
    333         }
    334         return null;
    335     }
    336 
    337     /**
    338      * Removes attached listeners.
    339      *
    340      * @see WorkbenchPart
    341      */
    342     @Override
    343     public void dispose() {
    344         ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    345 
    346         super.dispose();
    347     }
    348 
    349     /**
    350      * Commit all dirty pages then saves the contents of the text editor.
    351      * <p/>
    352      * This works by committing all data to the XML model and then
    353      * asking the Structured XML Editor to save the XML.
    354      *
    355      * @see IEditorPart
    356      */
    357     @Override
    358     public void doSave(IProgressMonitor monitor) {
    359         commitPages(true /* onSave */);
    360 
    361         // The actual "save" operation is done by the Structured XML Editor
    362         getEditor(mTextPageIndex).doSave(monitor);
    363     }
    364 
    365     /* (non-Javadoc)
    366      * Saves the contents of this editor to another object.
    367      * <p>
    368      * Subclasses must override this method to implement the open-save-close lifecycle
    369      * for an editor.  For greater details, see <code>IEditorPart</code>
    370      * </p>
    371      *
    372      * @see IEditorPart
    373      */
    374     @Override
    375     public void doSaveAs() {
    376         commitPages(true /* onSave */);
    377 
    378         IEditorPart editor = getEditor(mTextPageIndex);
    379         editor.doSaveAs();
    380         setPageText(mTextPageIndex, editor.getTitle());
    381         setInput(editor.getEditorInput());
    382     }
    383 
    384     /**
    385      * Commits all dirty pages in the editor. This method should
    386      * be called as a first step of a 'save' operation.
    387      * <p/>
    388      * This is the same implementation as in {@link FormEditor}
    389      * except it fixes two bugs: a cast to IFormPage is done
    390      * from page.get(i) <em>before</em> being tested with instanceof.
    391      * Another bug is that the last page might be a null pointer.
    392      * <p/>
    393      * The incorrect casting makes the original implementation crash due
    394      * to our {@link StructuredTextEditor} not being an {@link IFormPage}
    395      * so we have to override and duplicate to fix it.
    396      *
    397      * @param onSave <code>true</code> if commit is performed as part
    398      * of the 'save' operation, <code>false</code> otherwise.
    399      * @since 3.3
    400      */
    401     @Override
    402     public void commitPages(boolean onSave) {
    403         if (pages != null) {
    404             for (int i = 0; i < pages.size(); i++) {
    405                 Object page = pages.get(i);
    406                 if (page != null && page instanceof IFormPage) {
    407                     IFormPage form_page = (IFormPage) page;
    408                     IManagedForm managed_form = form_page.getManagedForm();
    409                     if (managed_form != null && managed_form.isDirty()) {
    410                         managed_form.commit(onSave);
    411                     }
    412                 }
    413             }
    414         }
    415     }
    416 
    417     /* (non-Javadoc)
    418      * Returns whether the "save as" operation is supported by this editor.
    419      * <p>
    420      * Subclasses must override this method to implement the open-save-close lifecycle
    421      * for an editor.  For greater details, see <code>IEditorPart</code>
    422      * </p>
    423      *
    424      * @see IEditorPart
    425      */
    426     @Override
    427     public boolean isSaveAsAllowed() {
    428         return false;
    429     }
    430 
    431     // ---- Local methods ----
    432 
    433 
    434     /**
    435      * Helper method that creates a new hyper-link Listener.
    436      * Used by derived classes which need active links in {@link FormText}.
    437      * <p/>
    438      * This link listener handles two kinds of URLs:
    439      * <ul>
    440      * <li> Links starting with "http" are simply sent to a local browser.
    441      * <li> Links starting with "file:/" are simply sent to a local browser.
    442      * <li> Links starting with "page:" are expected to be an editor page id to switch to.
    443      * <li> Other links are ignored.
    444      * </ul>
    445      *
    446      * @return A new hyper-link listener for FormText to use.
    447      */
    448     public final IHyperlinkListener createHyperlinkListener() {
    449         return new HyperlinkAdapter() {
    450             /**
    451              * Switch to the page corresponding to the link that has just been clicked.
    452              * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
    453              */
    454             @Override
    455             public void linkActivated(HyperlinkEvent e) {
    456                 super.linkActivated(e);
    457                 String link = e.data.toString();
    458                 if (link.startsWith("http") ||          //$NON-NLS-1$
    459                         link.startsWith("file:/")) {    //$NON-NLS-1$
    460                     openLinkInBrowser(link);
    461                 } else if (link.startsWith("page:")) {  //$NON-NLS-1$
    462                     // Switch to an internal page
    463                     setActivePage(link.substring(5 /* strlen("page:") */));
    464                 }
    465             }
    466         };
    467     }
    468 
    469     /**
    470      * Open the http link into a browser
    471      *
    472      * @param link The URL to open in a browser
    473      */
    474     private void openLinkInBrowser(String link) {
    475         try {
    476             IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
    477             wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
    478         } catch (PartInitException e1) {
    479             // pass
    480         } catch (MalformedURLException e1) {
    481             // pass
    482         }
    483     }
    484 
    485     /**
    486      * Creates the XML source editor.
    487      * <p/>
    488      * Memorizes the index page of the source editor (it's always the last page, but the number
    489      * of pages before can change.)
    490      * <br/>
    491      * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
    492      * Finally triggers modelChanged() on the model listener -- derived classes can use this
    493      * to initialize the model the first time.
    494      * <p/>
    495      * Called only once <em>after</em> createFormPages.
    496      */
    497     private void createTextEditor() {
    498         try {
    499             mTextEditor = new TextEditor();
    500             int index = addPage(mTextEditor, getEditorInput());
    501             mTextPageIndex = index;
    502             setPageText(index, mTextEditor.getTitle());
    503 
    504             IDocumentProvider provider = mTextEditor.getDocumentProvider();
    505             mDocument = provider.getDocument(getEditorInput());
    506 
    507             mDocument.addDocumentListener(new IDocumentListener() {
    508                 @Override
    509                 public void documentChanged(DocumentEvent event) {
    510                     onDocumentChanged(event);
    511                 }
    512 
    513                 @Override
    514                 public void documentAboutToBeChanged(DocumentEvent event) {
    515                     // ignore
    516                 }
    517             });
    518 
    519 
    520         } catch (PartInitException e) {
    521             ErrorDialog.openError(getSite().getShell(),
    522                     "Android Text Editor Error", null, e.getStatus());
    523         }
    524     }
    525 
    526     /**
    527      * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
    528      * the current file input.
    529      * <p/>
    530      * All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}.
    531      * The actual document instance is a {@link SynchronizableDocument}, which creates a lock
    532      * around read/set operations. The base API provided by {@link IDocument} provides ways to
    533      * manipulate the document line per line or as a bulk.
    534      */
    535     public IDocument getDocument() {
    536         return mDocument;
    537     }
    538 
    539     /**
    540      * Returns the {@link IProject} for the edited file.
    541      */
    542     public IProject getProject() {
    543         if (mTextEditor != null) {
    544             IEditorInput input = mTextEditor.getEditorInput();
    545             if (input instanceof FileEditorInput) {
    546                 FileEditorInput fileInput = (FileEditorInput)input;
    547                 IFile inputFile = fileInput.getFile();
    548 
    549                 if (inputFile != null) {
    550                     return inputFile.getProject();
    551                 }
    552             }
    553         }
    554 
    555         return null;
    556     }
    557 
    558     /**
    559      * Runs the given operation in the context of a document RewriteSession.
    560      * Takes care of properly starting and stopping the operation.
    561      * <p/>
    562      * The operation itself should just access {@link #getDocument()} and use the
    563      * normal document's API to manipulate it.
    564      *
    565      * @see #getDocument()
    566      */
    567     public void wrapRewriteSession(Runnable operation) {
    568         if (mDocument instanceof IDocumentExtension4) {
    569             IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
    570 
    571             DocumentRewriteSession session = null;
    572             try {
    573                 session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
    574 
    575                 operation.run();
    576             } catch(IllegalStateException e) {
    577                 AdtPlugin.log(e, "wrapRewriteSession failed");
    578                 e.printStackTrace();
    579             } finally {
    580                 if (session != null) {
    581                     doc4.stopRewriteSession(session);
    582                 }
    583             }
    584 
    585         } else {
    586             // Not an IDocumentExtension4? Unlikely. Try the operation anyway.
    587             operation.run();
    588         }
    589     }
    590 
    591 }
    592