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      * 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 }
    573