1 /* 2 * Copyright (C) 2007 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.manifest.pages; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 21 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; 22 import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart; 23 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; 24 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState; 25 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 26 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 27 import com.android.utils.SdkUtils; 28 29 import org.eclipse.swt.SWT; 30 import org.eclipse.swt.events.SelectionAdapter; 31 import org.eclipse.swt.events.SelectionEvent; 32 import org.eclipse.swt.widgets.Button; 33 import org.eclipse.swt.widgets.Composite; 34 import org.eclipse.ui.forms.IManagedForm; 35 import org.eclipse.ui.forms.widgets.FormText; 36 import org.eclipse.ui.forms.widgets.FormToolkit; 37 import org.eclipse.ui.forms.widgets.Section; 38 import org.eclipse.ui.forms.widgets.TableWrapData; 39 import org.w3c.dom.Document; 40 import org.w3c.dom.Node; 41 import org.w3c.dom.Text; 42 43 /** 44 * Appllication Toogle section part for application page. 45 */ 46 final class ApplicationToggle extends UiElementPart { 47 48 /** Checkbox indicating whether an application node is present */ 49 private Button mCheckbox; 50 /** Listen to changes to the UI node for <application> and updates the checkbox */ 51 private AppNodeUpdateListener mAppNodeUpdateListener; 52 /** Internal flag to know where we're programmatically modifying the checkbox and we want to 53 * avoid triggering the checkbox's callback. */ 54 public boolean mInternalModification; 55 private FormText mTooltipFormText; 56 57 public ApplicationToggle(Composite body, FormToolkit toolkit, ManifestEditor editor, 58 UiElementNode applicationUiNode) { 59 super(body, toolkit, editor, applicationUiNode, 60 "Application Toggle", 61 null, /* description */ 62 Section.TWISTIE | Section.EXPANDED); 63 } 64 65 @Override 66 public void dispose() { 67 super.dispose(); 68 if (getUiElementNode() != null && mAppNodeUpdateListener != null) { 69 getUiElementNode().removeUpdateListener(mAppNodeUpdateListener); 70 mAppNodeUpdateListener = null; 71 } 72 } 73 74 /** 75 * Changes and refreshes the Application UI node handle by the this part. 76 */ 77 @Override 78 public void setUiElementNode(UiElementNode uiElementNode) { 79 super.setUiElementNode(uiElementNode); 80 81 updateTooltip(); 82 83 // Set the state of the checkbox 84 mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(), 85 UiUpdateState.CHILDREN_CHANGED); 86 } 87 88 /** 89 * Create the controls to edit the attributes for the given ElementDescriptor. 90 * <p/> 91 * This MUST not be called by the constructor. Instead it must be called from 92 * <code>initialize</code> (i.e. right after the form part is added to the managed form.) 93 * 94 * @param managedForm The owner managed form 95 */ 96 @Override 97 protected void createFormControls(IManagedForm managedForm) { 98 FormToolkit toolkit = managedForm.getToolkit(); 99 Composite table = createTableLayout(toolkit, 1 /* numColumns */); 100 101 mTooltipFormText = createFormText(table, toolkit, true, "<form></form>", 102 false /* setupLayoutData */); 103 updateTooltip(); 104 105 mCheckbox = toolkit.createButton(table, 106 "Define an <application> tag in the AndroidManifest.xml", 107 SWT.CHECK); 108 mCheckbox.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP)); 109 mCheckbox.setSelection(false); 110 mCheckbox.addSelectionListener(new CheckboxSelectionListener()); 111 112 mAppNodeUpdateListener = new AppNodeUpdateListener(); 113 getUiElementNode().addUpdateListener(mAppNodeUpdateListener); 114 115 // Initialize the state of the checkbox 116 mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(), 117 UiUpdateState.CHILDREN_CHANGED); 118 119 // Tell the section that the layout has changed. 120 layoutChanged(); 121 } 122 123 /** 124 * Updates the application tooltip in the form text. 125 * If there is no tooltip, the form text is hidden. 126 */ 127 private void updateTooltip() { 128 boolean isVisible = false; 129 130 String tooltip = getUiElementNode().getDescriptor().getTooltip(); 131 if (tooltip != null) { 132 tooltip = DescriptorsUtils.formatFormText(tooltip, 133 getUiElementNode().getDescriptor(), 134 Sdk.getCurrent().getDocumentationBaseUrl()); 135 136 mTooltipFormText.setText(tooltip, true /* parseTags */, true /* expandURLs */); 137 mTooltipFormText.setImage(DescriptorsUtils.IMAGE_KEY, AdtPlugin.getAndroidLogo()); 138 mTooltipFormText.addHyperlinkListener(getEditor().createHyperlinkListener()); 139 isVisible = true; 140 } 141 142 mTooltipFormText.setVisible(isVisible); 143 } 144 145 /** 146 * This listener synchronizes the XML application node when the checkbox 147 * is changed by the user. 148 */ 149 private class CheckboxSelectionListener extends SelectionAdapter { 150 private Node mUndoXmlNode; 151 private Node mUndoXmlParent; 152 private Node mUndoXmlNextNode; 153 private Node mUndoXmlNextElement; 154 private Document mUndoXmlDocument; 155 156 @Override 157 public void widgetSelected(SelectionEvent e) { 158 super.widgetSelected(e); 159 if (!mInternalModification && getUiElementNode() != null) { 160 getUiElementNode().getEditor().wrapUndoEditXmlModel( 161 mCheckbox.getSelection() 162 ? "Create or restore Application node" 163 : "Remove Application node", 164 new Runnable() { 165 @Override 166 public void run() { 167 if (mCheckbox.getSelection()) { 168 // The user wants an <application> node. 169 // Either restore a previous one 170 // or create a full new one. 171 boolean create = true; 172 if (mUndoXmlNode != null) { 173 create = !restoreApplicationNode(); 174 } 175 if (create) { 176 getUiElementNode().createXmlNode(); 177 } 178 } else { 179 // Users no longer wants the <application> node. 180 removeApplicationNode(); 181 } 182 } 183 }); 184 } 185 } 186 187 /** 188 * Restore a previously "saved" application node. 189 * 190 * @return True if the node could be restored, false otherwise. 191 */ 192 private boolean restoreApplicationNode() { 193 if (mUndoXmlDocument == null || mUndoXmlNode == null) { 194 return false; 195 } 196 197 // Validate node references... 198 mUndoXmlParent = validateNode(mUndoXmlDocument, mUndoXmlParent); 199 mUndoXmlNextNode = validateNode(mUndoXmlDocument, mUndoXmlNextNode); 200 mUndoXmlNextElement = validateNode(mUndoXmlDocument, mUndoXmlNextElement); 201 202 if (mUndoXmlParent == null){ 203 // If the parent node doesn't exist, try to find a new manifest node. 204 // If it doesn't exist, create it. 205 mUndoXmlParent = getUiElementNode().getUiParent().prepareCommit(); 206 mUndoXmlNextNode = null; 207 mUndoXmlNextElement = null; 208 } 209 210 boolean success = false; 211 if (mUndoXmlParent != null) { 212 // If the parent is still around, reuse the same node. 213 214 // Ideally we want to insert the node before what used to be its next sibling. 215 // If that's not possible, we try to insert it before its next sibling element. 216 // If that's not possible either, it will be inserted at the end of the parent's. 217 Node next = mUndoXmlNextNode; 218 if (next == null) { 219 next = mUndoXmlNextElement; 220 } 221 mUndoXmlParent.insertBefore(mUndoXmlNode, next); 222 if (next == null) { 223 Text sep = mUndoXmlDocument.createTextNode(SdkUtils.getLineSeparator()); 224 mUndoXmlParent.insertBefore(sep, null); // insert separator before end tag 225 } 226 success = true; 227 } 228 229 // Remove internal references to avoid using them twice 230 mUndoXmlParent = null; 231 mUndoXmlNextNode = null; 232 mUndoXmlNextElement = null; 233 mUndoXmlNode = null; 234 mUndoXmlDocument = null; 235 return success; 236 } 237 238 /** 239 * Validates that the given xml_node is still either the root node or one of its 240 * direct descendants. 241 * 242 * @param root_node The root of the node hierarchy to examine. 243 * @param xml_node The XML node to find. 244 * @return Returns xml_node if it is, otherwise returns null. 245 */ 246 private Node validateNode(Node root_node, Node xml_node) { 247 if (root_node == xml_node) { 248 return xml_node; 249 } else { 250 for (Node node = root_node.getFirstChild(); node != null; 251 node = node.getNextSibling()) { 252 if (root_node == xml_node || validateNode(node, xml_node) != null) { 253 return xml_node; 254 } 255 } 256 } 257 return null; 258 } 259 260 /** 261 * Removes the <application> node from the hierarchy. 262 * Before doing that, we try to remember where it was so that we can put it back 263 * in the same place. 264 */ 265 private void removeApplicationNode() { 266 // Make sure the node actually exists... 267 Node xml_node = getUiElementNode().getXmlNode(); 268 if (xml_node == null) { 269 return; 270 } 271 272 // Save its parent, next sibling and next element 273 mUndoXmlDocument = xml_node.getOwnerDocument(); 274 mUndoXmlParent = xml_node.getParentNode(); 275 mUndoXmlNextNode = xml_node.getNextSibling(); 276 mUndoXmlNextElement = mUndoXmlNextNode; 277 while (mUndoXmlNextElement != null && 278 mUndoXmlNextElement.getNodeType() != Node.ELEMENT_NODE) { 279 mUndoXmlNextElement = mUndoXmlNextElement.getNextSibling(); 280 } 281 282 // Actually remove the node from the hierarchy and keep it here. 283 // The returned node looses its parents/siblings pointers. 284 mUndoXmlNode = getUiElementNode().deleteXmlNode(); 285 } 286 } 287 288 /** 289 * This listener synchronizes the UI (i.e. the checkbox) with the 290 * actual presence of the application XML node. 291 */ 292 private class AppNodeUpdateListener implements IUiUpdateListener { 293 @Override 294 public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { 295 // The UiElementNode for the application XML node always exists, even 296 // if there is no corresponding XML node in the XML file. 297 // 298 // To update the checkbox to reflect the actual state, we just need 299 // to check if the XML node is null. 300 try { 301 mInternalModification = true; 302 boolean exists = ui_node.getXmlNode() != null; 303 if (mCheckbox.getSelection() != exists) { 304 mCheckbox.setSelection(exists); 305 } 306 } finally { 307 mInternalModification = false; 308 } 309 310 } 311 } 312 } 313