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.ui.tree; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 21 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; 22 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 24 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor; 25 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; 26 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper; 27 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; 28 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; 29 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; 30 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 31 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 32 33 import org.eclipse.core.runtime.IStatus; 34 import org.eclipse.jface.viewers.ISelection; 35 import org.eclipse.jface.viewers.ITreeSelection; 36 import org.eclipse.swt.events.DisposeEvent; 37 import org.eclipse.swt.events.DisposeListener; 38 import org.eclipse.swt.graphics.Image; 39 import org.eclipse.swt.widgets.Composite; 40 import org.eclipse.swt.widgets.Control; 41 import org.eclipse.ui.forms.IDetailsPage; 42 import org.eclipse.ui.forms.IFormPart; 43 import org.eclipse.ui.forms.IManagedForm; 44 import org.eclipse.ui.forms.events.ExpansionEvent; 45 import org.eclipse.ui.forms.events.IExpansionListener; 46 import org.eclipse.ui.forms.widgets.FormText; 47 import org.eclipse.ui.forms.widgets.FormToolkit; 48 import org.eclipse.ui.forms.widgets.Section; 49 import org.eclipse.ui.forms.widgets.SharedScrolledComposite; 50 import org.eclipse.ui.forms.widgets.TableWrapData; 51 import org.eclipse.ui.forms.widgets.TableWrapLayout; 52 53 import java.util.Collection; 54 import java.util.HashSet; 55 56 /** 57 * Details page for the {@link UiElementNode} nodes in the tree view. 58 * <p/> 59 * See IDetailsBase for more details. 60 */ 61 class UiElementDetail implements IDetailsPage { 62 63 /** The master-detail part, composed of a main tree and an auxiliary detail part */ 64 private ManifestSectionPart mMasterPart; 65 66 private Section mMasterSection; 67 private UiElementNode mCurrentUiElementNode; 68 private Composite mCurrentTable; 69 private boolean mIsDirty; 70 71 private IManagedForm mManagedForm; 72 73 private final UiTreeBlock mTree; 74 UiElementDetail(UiTreeBlock tree)75 public UiElementDetail(UiTreeBlock tree) { 76 mTree = tree; 77 mMasterPart = mTree.getMasterPart(); 78 mManagedForm = mMasterPart.getManagedForm(); 79 } 80 81 /* (non-java doc) 82 * Initializes the part. 83 */ 84 @Override initialize(IManagedForm form)85 public void initialize(IManagedForm form) { 86 mManagedForm = form; 87 } 88 89 /* (non-java doc) 90 * Creates the contents of the page in the provided parent. 91 */ 92 @Override createContents(Composite parent)93 public void createContents(Composite parent) { 94 mMasterSection = createMasterSection(parent); 95 } 96 97 /* (non-java doc) 98 * Called when the provided part has changed selection state. 99 * <p/> 100 * Only reply when our master part originates the selection. 101 */ 102 @Override selectionChanged(IFormPart part, ISelection selection)103 public void selectionChanged(IFormPart part, ISelection selection) { 104 if (part == mMasterPart && 105 !selection.isEmpty() && 106 selection instanceof ITreeSelection) { 107 ITreeSelection tree_selection = (ITreeSelection) selection; 108 109 Object first = tree_selection.getFirstElement(); 110 if (first instanceof UiElementNode) { 111 UiElementNode ui_node = (UiElementNode) first; 112 createUiAttributeControls(mManagedForm, ui_node); 113 } 114 } 115 } 116 117 /* (non-java doc) 118 * Instructs it to commit the new (modified) data back into the model. 119 */ 120 @Override commit(boolean onSave)121 public void commit(boolean onSave) { 122 123 mTree.getEditor().wrapEditXmlModel(new Runnable() { 124 @Override 125 public void run() { 126 try { 127 if (mCurrentUiElementNode != null) { 128 mCurrentUiElementNode.commit(); 129 } 130 131 // Finally reset the dirty flag if everything was saved properly 132 mIsDirty = false; 133 } catch (Exception e) { 134 AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$ 135 } 136 } 137 }); 138 } 139 140 @Override dispose()141 public void dispose() { 142 // pass 143 } 144 145 146 /* (non-java doc) 147 * Returns true if the part has been modified with respect to the data 148 * loaded from the model. 149 */ 150 @Override isDirty()151 public boolean isDirty() { 152 if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) { 153 markDirty(); 154 } 155 return mIsDirty; 156 } 157 158 @Override isStale()159 public boolean isStale() { 160 // pass 161 return false; 162 } 163 164 /** 165 * Called by the master part when the tree is refreshed after the framework resources 166 * have been reloaded. 167 */ 168 @Override refresh()169 public void refresh() { 170 if (mCurrentTable != null) { 171 mCurrentTable.dispose(); 172 mCurrentTable = null; 173 } 174 mCurrentUiElementNode = null; 175 mMasterSection.getParent().pack(true /* changed */); 176 } 177 178 @Override setFocus()179 public void setFocus() { 180 // pass 181 } 182 183 @Override setFormInput(Object input)184 public boolean setFormInput(Object input) { 185 // pass 186 return false; 187 } 188 189 /** 190 * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section. 191 * 192 * All the UI should be created in a layout which parent is the mSection itself. 193 * The hierarchy is: 194 * <pre> 195 * DetailPage 196 * + TableWrapLayout 197 * + Section (with title/description && fill_grab horizontal) 198 * + TableWrapLayout [*] 199 * + Labels/Forms/etc... [*] 200 * </pre> 201 * Both items marked with [*] are created by the derived classes to fit their needs. 202 * 203 * @param parent Parent of the mSection (from createContents) 204 * @return The new Section 205 */ createMasterSection(Composite parent)206 private Section createMasterSection(Composite parent) { 207 TableWrapLayout layout = new TableWrapLayout(); 208 layout.topMargin = 0; 209 parent.setLayout(layout); 210 211 FormToolkit toolkit = mManagedForm.getToolkit(); 212 Section section = toolkit.createSection(parent, Section.TITLE_BAR); 213 section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP)); 214 return section; 215 } 216 217 /** 218 * Create the ui attribute controls to edit the attributes for the given 219 * ElementDescriptor. 220 * <p/> 221 * This is called by the constructor. 222 * Derived classes can override this if necessary. 223 * 224 * @param managedForm The managed form 225 */ createUiAttributeControls( final IManagedForm managedForm, final UiElementNode ui_node)226 private void createUiAttributeControls( 227 final IManagedForm managedForm, 228 final UiElementNode ui_node) { 229 230 final ElementDescriptor elem_desc = ui_node.getDescriptor(); 231 mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription())); 232 233 if (mCurrentUiElementNode != ui_node) { 234 // Before changing the table, commit all dirty state. 235 if (mIsDirty) { 236 commit(false); 237 } 238 if (mCurrentTable != null) { 239 mCurrentTable.dispose(); 240 mCurrentTable = null; 241 } 242 243 // To iterate over all attributes, we use the {@link ElementDescriptor} instead 244 // of the {@link UiElementNode} because the attributes order is guaranteed in the 245 // descriptor but not in the node itself. 246 AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors(); 247 248 // If the attribute list contains at least one SeparatorAttributeDescriptor, 249 // sub-sections will be used. This needs to be known early as it influences the 250 // creation of the master table. 251 boolean useSubsections = false; 252 for (AttributeDescriptor attr_desc : attr_desc_list) { 253 if (attr_desc instanceof SeparatorAttributeDescriptor) { 254 // Sub-sections will be used. The default sections should no longer be 255 useSubsections = true; 256 break; 257 } 258 } 259 260 FormToolkit toolkit = managedForm.getToolkit(); 261 Composite masterTable = SectionHelper.createTableLayout(mMasterSection, 262 toolkit, useSubsections ? 1 : 2 /* numColumns */); 263 mCurrentTable = masterTable; 264 265 mCurrentUiElementNode = ui_node; 266 267 if (elem_desc.getTooltip() != null) { 268 String tooltip; 269 if (Sdk.getCurrent() != null && 270 Sdk.getCurrent().getDocumentationBaseUrl() != null) { 271 tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(), 272 elem_desc, 273 Sdk.getCurrent().getDocumentationBaseUrl()); 274 } else { 275 tooltip = elem_desc.getTooltip(); 276 } 277 278 try { 279 FormText text = SectionHelper.createFormText(masterTable, toolkit, 280 true /* isHtml */, tooltip, true /* setupLayoutData */); 281 text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener()); 282 Image icon = elem_desc.getCustomizedIcon(); 283 if (icon != null) { 284 text.setImage(DescriptorsUtils.IMAGE_KEY, icon); 285 } 286 } catch(Exception e) { 287 // The FormText parser is really really basic and will fail as soon as the 288 // HTML javadoc is ever so slightly malformatted. 289 AdtPlugin.log(e, 290 "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$ 291 ui_node.getDescriptor().getXmlName(), 292 tooltip); 293 294 // Fallback to a pure text tooltip, no fancy HTML 295 tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip()); 296 SectionHelper.createLabel(masterTable, toolkit, tooltip, tooltip); 297 } 298 } 299 300 Composite table = useSubsections ? null : masterTable; 301 302 for (AttributeDescriptor attr_desc : attr_desc_list) { 303 if (attr_desc instanceof XmlnsAttributeDescriptor) { 304 // Do not show hidden attributes 305 continue; 306 } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) { 307 String title = null; 308 if (attr_desc instanceof SeparatorAttributeDescriptor) { 309 // xmlName is actually the label of the separator 310 title = attr_desc.getXmlLocalName(); 311 } else { 312 title = String.format("Attributes from %1$s", elem_desc.getUiName()); 313 } 314 315 table = createSubSectionTable(toolkit, masterTable, title); 316 if (attr_desc instanceof SeparatorAttributeDescriptor) { 317 continue; 318 } 319 } 320 321 UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc); 322 323 if (ui_attr != null) { 324 ui_attr.createUiControl(table, managedForm); 325 326 if (ui_attr.getCurrentValue() != null && 327 ui_attr.getCurrentValue().length() > 0) { 328 ((Section) table.getParent()).setExpanded(true); 329 } 330 } else { 331 // The XML has an extra unknown attribute. 332 // This is not expected to happen so it is ignored. 333 AdtPlugin.log(IStatus.INFO, 334 "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$ 335 attr_desc.getXmlLocalName(), 336 ui_node.getDescriptor().getXmlName()); 337 } 338 } 339 340 // Create a sub-section for the unknown attributes. 341 // It is initially hidden till there are some attributes to show here. 342 final Composite unknownTable = createSubSectionTable(toolkit, masterTable, 343 "Unknown XML Attributes"); 344 unknownTable.getParent().setVisible(false); // set section to not visible 345 final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>(); 346 347 final IUiUpdateListener updateListener = new IUiUpdateListener() { 348 @Override 349 public void uiElementNodeUpdated(UiElementNode uiNode, UiUpdateState state) { 350 if (state == UiUpdateState.ATTR_UPDATED) { 351 updateUnknownAttributesSection(uiNode, unknownTable, managedForm, 352 reference); 353 } 354 } 355 }; 356 ui_node.addUpdateListener(updateListener); 357 358 // remove the listener when the UI is disposed 359 unknownTable.addDisposeListener(new DisposeListener() { 360 @Override 361 public void widgetDisposed(DisposeEvent e) { 362 ui_node.removeUpdateListener(updateListener); 363 } 364 }); 365 366 updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference); 367 368 mMasterSection.getParent().pack(true /* changed */); 369 } 370 } 371 372 /** 373 * Create a sub Section and its embedding wrapper table with 2 columns. 374 * @return The table, child of a new section. 375 */ createSubSectionTable(FormToolkit toolkit, Composite masterTable, String title)376 private Composite createSubSectionTable(FormToolkit toolkit, 377 Composite masterTable, String title) { 378 379 // The Section composite seems to ignore colspan when assigned a TableWrapData so 380 // if the parent is a table with more than one column an extra table with one column 381 // is inserted to respect colspan. 382 int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns; 383 if (parentNumCol > 1) { 384 masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1); 385 TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB); 386 twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT; 387 twd.colspan = parentNumCol; 388 masterTable.setLayoutData(twd); 389 } 390 391 Composite table; 392 Section section = toolkit.createSection(masterTable, 393 Section.TITLE_BAR | Section.TWISTIE); 394 395 // Add an expansion listener that will trigger a reflow on the parent 396 // ScrolledPageBook (which is actually a SharedScrolledComposite). This will 397 // recompute the correct size and adjust the scrollbar as needed. 398 section.addExpansionListener(new IExpansionListener() { 399 @Override 400 public void expansionStateChanged(ExpansionEvent e) { 401 reflowMasterSection(); 402 } 403 404 @Override 405 public void expansionStateChanging(ExpansionEvent e) { 406 // pass 407 } 408 }); 409 410 section.setText(title); 411 section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, 412 TableWrapData.TOP)); 413 table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */); 414 return table; 415 } 416 417 /** 418 * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite). 419 * This will recompute the correct size and adjust the scrollbar as needed. 420 */ reflowMasterSection()421 private void reflowMasterSection() { 422 for(Composite c = mMasterSection; c != null; c = c.getParent()) { 423 if (c instanceof SharedScrolledComposite) { 424 ((SharedScrolledComposite) c).reflow(true /* flushCache */); 425 break; 426 } 427 } 428 } 429 430 /** 431 * Updates the unknown attributes section for the UI Node. 432 */ updateUnknownAttributesSection(UiElementNode ui_node, final Composite unknownTable, final IManagedForm managedForm, HashSet<UiAttributeNode> reference)433 private void updateUnknownAttributesSection(UiElementNode ui_node, 434 final Composite unknownTable, final IManagedForm managedForm, 435 HashSet<UiAttributeNode> reference) { 436 Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes(); 437 Section section = ((Section) unknownTable.getParent()); 438 boolean needs_reflow = false; 439 440 // The table was created hidden, show it if there are unknown attributes now 441 if (ui_attrs.size() > 0 && !section.isVisible()) { 442 section.setVisible(true); 443 needs_reflow = true; 444 } 445 446 // Compare the new attribute set with the old "reference" one 447 boolean has_differences = ui_attrs.size() != reference.size(); 448 if (!has_differences) { 449 for (UiAttributeNode ui_attr : ui_attrs) { 450 if (!reference.contains(ui_attr)) { 451 has_differences = true; 452 break; 453 } 454 } 455 } 456 457 if (has_differences) { 458 needs_reflow = true; 459 reference.clear(); 460 461 // Remove all children of the table 462 for (Control c : unknownTable.getChildren()) { 463 c.dispose(); 464 } 465 466 // Recreate all attributes UI 467 for (UiAttributeNode ui_attr : ui_attrs) { 468 reference.add(ui_attr); 469 ui_attr.createUiControl(unknownTable, managedForm); 470 471 if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) { 472 section.setExpanded(true); 473 } 474 } 475 } 476 477 if (needs_reflow) { 478 reflowMasterSection(); 479 } 480 } 481 482 /** 483 * Marks the part dirty. Called as a result of user interaction with the widgets in the 484 * section. 485 */ markDirty()486 private void markDirty() { 487 if (!mIsDirty) { 488 mIsDirty = true; 489 mManagedForm.dirtyStateChanged(); 490 } 491 } 492 } 493 494 495