aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java1331
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java92
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidOutlineConfiguration.java37
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidQuickOutlineConfiguration.java35
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java243
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java591
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java460
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlCharacterMatcher.java238
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java1709
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java229
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java1893
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IPageImageProvider.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java448
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java117
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/XmlEditorMultiOutline.java221
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimDescriptors.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java168
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationEditorDelegate.java173
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimatorDescriptors.java184
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLDescriber.java80
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLMultiPageEditorPart.java85
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/FileStorage.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/XmlStorageEditorInput.java119
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorContentAssist.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorDescriptors.java98
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorEditorDelegate.java116
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonActionContributor.java40
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java84
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonSourceViewerConfig.java66
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java249
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java467
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptor.java121
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptorLabelProvider.java87
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/BooleanAttributeDescriptor.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java961
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DocumentDescriptor.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java485
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/EnumAttributeDescriptor.java42
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/FlagAttributeDescriptor.java92
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IDescriptorProvider.java24
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ITextAttributeCreator.java47
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IUnknownDescriptorProvider.java38
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ListAttributeDescriptor.java89
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java108
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/SeparatorAttributeDescriptor.java45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextAttributeDescriptor.java290
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextValueDescriptor.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/XmlnsAttributeDescriptor.java77
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java233
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java169
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java882
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java97
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java774
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java79
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java357
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java267
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableContentAssist.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableDescriptors.java301
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableEditorDelegate.java153
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java356
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java107
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java124
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java113
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormatter.java83
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java754
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlFormatPreferences.java144
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java249
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatProcessor.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlQuickAssistManager.java106
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ActionBarHandler.java95
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/BasePullParser.java244
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java166
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ExplodedRenderingHelper.java421
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java234
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java1001
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java81
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java375
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java693
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java661
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/WidgetPullParser.java174
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java162
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java1091
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java2096
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java129
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java395
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java843
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java290
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java199
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java215
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java149
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java184
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java506
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java180
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java126
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java318
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java509
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java621
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java597
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java249
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java396
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java352
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java215
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java1178
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java429
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java195
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java132
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java395
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java203
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java915
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java87
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java654
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java96
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java304
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java645
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java930
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java217
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java182
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java2937
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java187
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java241
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java447
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java979
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java1111
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java150
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java732
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java1720
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java165
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java413
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java394
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java140
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java94
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java181
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java220
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java852
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java129
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java217
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java107
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java1439
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java91
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java1265
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java247
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java642
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java327
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java1333
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java222
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java1696
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java668
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java279
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java141
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java140
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java252
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java1262
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java247
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java370
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java154
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java75
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java319
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java457
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java771
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java762
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java86
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java517
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java192
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java876
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java856
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml452
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml382
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java118
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java77
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java217
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java750
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java329
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java403
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java182
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java186
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java278
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java121
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java548
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java657
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java195
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java298
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java199
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java670
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java126
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java579
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleWizard.java440
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java988
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/JavaQuickAssistant.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java336
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java1633
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java246
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapWizard.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableAction.java48
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java452
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableWizard.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java1403
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java170
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java76
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java439
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java268
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java208
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java94
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java578
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java957
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java628
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java106
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java123
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java56
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java89
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java89
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java736
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java132
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java331
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java321
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java175
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java136
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java312
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java102
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java123
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java87
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java165
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java111
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuEditorDelegate.java175
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuTreePage.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/descriptors/MenuDescriptors.java199
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlEditorDelegate.java135
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlTreePage.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/PlainXmlEditorDelegate.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/descriptors/OtherXmlDescriptors.java373
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java490
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java72
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java76
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java59
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java364
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java284
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java221
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java415
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java129
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java598
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java494
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java106
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java946
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java32
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java174
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java2160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java310
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java220
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java523
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java146
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java196
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java118
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java242
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesEditorDelegate.java144
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesTreePage.java108
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ColorValueDescriptor.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ItemElementDescriptor.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ValuesDescriptors.java337
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiColorValueNode.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiItemElementNode.java58
299 files changed, 97816 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
new file mode 100644
index 000000000..5aac51f68
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
@@ -0,0 +1,1331 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.PREFIX_ANDROID;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+import static com.android.SdkConstants.UNIT_DP;
+import static com.android.SdkConstants.UNIT_IN;
+import static com.android.SdkConstants.UNIT_MM;
+import static com.android.SdkConstants.UNIT_PT;
+import static com.android.SdkConstants.UNIT_PX;
+import static com.android.SdkConstants.UNIT_SP;
+import static com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor.ATTRIBUTE_ICON_FILENAME;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.utils.Pair;
+import com.android.utils.XmlUtils;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.ui.ISharedImages;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Content Assist Processor for Android XML files
+ * <p>
+ * Remaining corner cases:
+ * <ul>
+ * <li>Completion does not work right if there is a space between the = and the opening
+ * quote.
+ * <li>Replacement completion does not work right if the caret is to the left of the
+ * opening quote, where the opening quote is a single quote, and the replacement items use
+ * double quotes.
+ * </ul>
+ */
+@SuppressWarnings("restriction") // XML model
+public abstract class AndroidContentAssist implements IContentAssistProcessor {
+
+ /** Regexp to detect a full attribute after an element tag.
+ * <pre>Syntax:
+ * name = "..." quoted string with all but < and "
+ * or:
+ * name = '...' quoted string with all but < and '
+ * </pre>
+ */
+ private static Pattern sFirstAttribute = Pattern.compile(
+ "^ *[a-zA-Z_:]+ *= *(?:\"[^<\"]*\"|'[^<']*')"); //$NON-NLS-1$
+
+ /** Regexp to detect an element tag name */
+ private static Pattern sFirstElementWord = Pattern.compile("^[a-zA-Z0-9_:.-]+"); //$NON-NLS-1$
+
+ /** Regexp to detect whitespace */
+ private static Pattern sWhitespace = Pattern.compile("\\s+"); //$NON-NLS-1$
+
+ protected final static String ROOT_ELEMENT = "";
+
+ /** Descriptor of the root of the XML hierarchy. This a "fake" ElementDescriptor which
+ * is used to list all the possible roots given by actual implementations.
+ * DO NOT USE DIRECTLY. Call {@link #getRootDescriptor()} instead. */
+ private ElementDescriptor mRootDescriptor;
+
+ private final int mDescriptorId;
+
+ protected AndroidXmlEditor mEditor;
+
+ /**
+ * Constructor for AndroidContentAssist
+ * @param descriptorId An id for {@link AndroidTargetData#getDescriptorProvider(int)}.
+ * The Id can be one of {@link AndroidTargetData#DESCRIPTOR_MANIFEST},
+ * {@link AndroidTargetData#DESCRIPTOR_LAYOUT},
+ * {@link AndroidTargetData#DESCRIPTOR_MENU},
+ * or {@link AndroidTargetData#DESCRIPTOR_OTHER_XML}.
+ * All other values will throw an {@link IllegalArgumentException} later at runtime.
+ */
+ public AndroidContentAssist(int descriptorId) {
+ mDescriptorId = descriptorId;
+ }
+
+ /**
+ * Returns a list of completion proposals based on the
+ * specified location within the document that corresponds
+ * to the current cursor position within the text viewer.
+ *
+ * @param viewer the viewer whose document is used to compute the proposals
+ * @param offset an offset within the document for which completions should be computed
+ * @return an array of completion proposals or <code>null</code> if no proposals are possible
+ *
+ * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int)
+ */
+ @Override
+ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
+ String wordPrefix = extractElementPrefix(viewer, offset);
+
+ if (mEditor == null) {
+ mEditor = AndroidXmlEditor.fromTextViewer(viewer);
+ if (mEditor == null) {
+ // This should not happen. Duck and forget.
+ AdtPlugin.log(IStatus.ERROR, "Editor not found during completion");
+ return null;
+ }
+ }
+
+ // List of proposals, in the order presented to the user.
+ List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(80);
+
+ // Look up the caret context - where in an element, or between elements, or
+ // within an element's children, is the given caret offset located?
+ Pair<Node, Node> context = DomUtilities.getNodeContext(viewer.getDocument(), offset);
+ if (context == null) {
+ return null;
+ }
+ Node parentNode = context.getFirst();
+ Node currentNode = context.getSecond();
+ assert parentNode != null || currentNode != null;
+
+ UiElementNode rootUiNode = mEditor.getUiRootNode();
+ if (currentNode == null || currentNode.getNodeType() == Node.TEXT_NODE) {
+ UiElementNode parentUiNode =
+ rootUiNode == null ? null : rootUiNode.findXmlNode(parentNode);
+ computeTextValues(proposals, offset, parentNode, currentNode, parentUiNode,
+ wordPrefix);
+ } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
+ String parent = currentNode.getNodeName();
+ AttribInfo info = parseAttributeInfo(viewer, offset, offset - wordPrefix.length());
+ char nextChar = extractChar(viewer, offset);
+ if (info != null) {
+ // check to see if we can find a UiElementNode matching this XML node
+ UiElementNode currentUiNode = rootUiNode == null
+ ? null : rootUiNode.findXmlNode(currentNode);
+ computeAttributeProposals(proposals, viewer, offset, wordPrefix, currentUiNode,
+ parentNode, currentNode, parent, info, nextChar);
+ } else {
+ computeNonAttributeProposals(viewer, offset, wordPrefix, proposals, parentNode,
+ currentNode, parent, nextChar);
+ }
+ }
+
+ return proposals.toArray(new ICompletionProposal[proposals.size()]);
+ }
+
+ private void computeNonAttributeProposals(ITextViewer viewer, int offset, String wordPrefix,
+ List<ICompletionProposal> proposals, Node parentNode, Node currentNode, String parent,
+ char nextChar) {
+ if (startsWith(parent, wordPrefix)) {
+ // We are still editing the element's tag name, not the attributes
+ // (the element's tag name may not even be complete)
+
+ Object[] choices = getChoicesForElement(parent, currentNode);
+ if (choices == null || choices.length == 0) {
+ return;
+ }
+
+ int replaceLength = parent.length() - wordPrefix.length();
+ boolean isNew = replaceLength == 0 && nextNonspaceChar(viewer, offset) == '<';
+ // Special case: if we are right before the beginning of a new
+ // element, wipe out the replace length such that we insert before it,
+ // we don't edit the current element.
+ if (wordPrefix.length() == 0 && nextChar == '<') {
+ replaceLength = 0;
+ isNew = true;
+ }
+
+ // If we found some suggestions, do we need to add an opening "<" bracket
+ // for the element? We don't if the cursor is right after "<" or "</".
+ // Per XML Spec, there's no whitespace between "<" or "</" and the tag name.
+ char needTag = computeElementNeedTag(viewer, offset, wordPrefix);
+
+ addMatchingProposals(proposals, choices, offset,
+ parentNode != null ? parentNode : null, wordPrefix, needTag,
+ false /* isAttribute */, isNew, false /*isComplete*/,
+ replaceLength);
+ }
+ }
+
+ private void computeAttributeProposals(List<ICompletionProposal> proposals, ITextViewer viewer,
+ int offset, String wordPrefix, UiElementNode currentUiNode, Node parentNode,
+ Node currentNode, String parent, AttribInfo info, char nextChar) {
+ // We're editing attributes in an element node (either the attributes' names
+ // or their values).
+
+ if (info.isInValue) {
+ if (computeAttributeValues(proposals, offset, parent, info.name, currentNode,
+ wordPrefix, info.skipEndTag, info.replaceLength)) {
+ return;
+ }
+ }
+
+ // Look up attribute proposals based on descriptors
+ Object[] choices = getChoicesForAttribute(parent, currentNode, currentUiNode,
+ info, wordPrefix);
+ if (choices == null || choices.length == 0) {
+ return;
+ }
+
+ int replaceLength = info.replaceLength;
+ if (info.correctedPrefix != null) {
+ wordPrefix = info.correctedPrefix;
+ }
+ char needTag = info.needTag;
+ // Look to the right and see if we're followed by whitespace
+ boolean isNew = replaceLength == 0
+ && (Character.isWhitespace(nextChar) || nextChar == '>' || nextChar == '/');
+
+ addMatchingProposals(proposals, choices, offset, parentNode != null ? parentNode : null,
+ wordPrefix, needTag, true /* isAttribute */, isNew, info.skipEndTag,
+ replaceLength);
+ }
+
+ private char computeElementNeedTag(ITextViewer viewer, int offset, String wordPrefix) {
+ char needTag = 0;
+ int offset2 = offset - wordPrefix.length() - 1;
+ char c1 = extractChar(viewer, offset2);
+ if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) {
+ needTag = '<';
+ }
+ return needTag;
+ }
+
+ protected int computeTextReplaceLength(Node currentNode, int offset) {
+ if (currentNode == null) {
+ return 0;
+ }
+
+ assert currentNode != null && currentNode.getNodeType() == Node.TEXT_NODE;
+
+ String nodeValue = currentNode.getNodeValue();
+ int relativeOffset = offset - ((IndexedRegion) currentNode).getStartOffset();
+ int lineEnd = nodeValue.indexOf('\n', relativeOffset);
+ if (lineEnd == -1) {
+ lineEnd = nodeValue.length();
+ }
+ return lineEnd - relativeOffset;
+ }
+
+ /**
+ * Gets the choices when the user is editing the name of an XML element.
+ * <p/>
+ * The user is editing the name of an element (the "parent").
+ * Find the grand-parent and if one is found, return its children element list.
+ * The name which is being edited should be one of those.
+ * <p/>
+ * Example: <manifest><applic*cursor* => returns the list of all elements that
+ * can be found under <manifest>, of which <application> is one of the choices.
+ *
+ * @return an ElementDescriptor[] or null if no valid element was found.
+ */
+ protected Object[] getChoicesForElement(String parent, Node currentNode) {
+ ElementDescriptor grandparent = null;
+ if (currentNode.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
+ grandparent = getDescriptor(currentNode.getParentNode().getNodeName());
+ } else if (currentNode.getParentNode().getNodeType() == Node.DOCUMENT_NODE) {
+ grandparent = getRootDescriptor();
+ }
+ if (grandparent != null) {
+ for (ElementDescriptor e : grandparent.getChildren()) {
+ if (e.getXmlName().startsWith(parent)) {
+ return sort(grandparent.getChildren());
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Non-destructively sort a list of ElementDescriptors and return the result */
+ protected static ElementDescriptor[] sort(ElementDescriptor[] elements) {
+ if (elements != null && elements.length > 1) {
+ // Sort alphabetically. Must make copy to not destroy original.
+ ElementDescriptor[] copy = new ElementDescriptor[elements.length];
+ System.arraycopy(elements, 0, copy, 0, elements.length);
+
+ Arrays.sort(copy, new Comparator<ElementDescriptor>() {
+ @Override
+ public int compare(ElementDescriptor e1, ElementDescriptor e2) {
+ return e1.getXmlLocalName().compareTo(e2.getXmlLocalName());
+ }
+ });
+
+ return copy;
+ }
+
+ return elements;
+ }
+
+ /**
+ * Gets the choices when the user is editing an XML attribute.
+ * <p/>
+ * In input, attrInfo contains details on the analyzed context, namely whether the
+ * user is editing an attribute value (isInValue) or an attribute name.
+ * <p/>
+ * In output, attrInfo also contains two possible new values (this is a hack to circumvent
+ * the lack of out-parameters in Java):
+ * - AttribInfo.correctedPrefix if the user has been editing an attribute value and it has
+ * been detected that what the user typed is different from what extractElementPrefix()
+ * predicted. This happens because extractElementPrefix() stops when a character that
+ * cannot be an element name appears whereas parseAttributeInfo() uses a grammar more
+ * lenient as suitable for attribute values.
+ * - AttribInfo.needTag will be non-zero if we find that the attribute completion proposal
+ * must be double-quoted.
+ * @param currentUiNode
+ *
+ * @return an AttributeDescriptor[] if the user is editing an attribute name.
+ * a String[] if the user is editing an attribute value with some known values,
+ * or null if nothing is known about the context.
+ */
+ private Object[] getChoicesForAttribute(
+ String parent, Node currentNode, UiElementNode currentUiNode, AttribInfo attrInfo,
+ String wordPrefix) {
+ Object[] choices = null;
+ if (attrInfo.isInValue) {
+ // Editing an attribute's value... Get the attribute name and then the
+ // possible choices for the tuple(parent,attribute)
+ String value = attrInfo.valuePrefix;
+ if (value.startsWith("'") || value.startsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$
+ value = value.substring(1);
+ // The prefix that was found at the beginning only scan for characters
+ // valid for tag name. We now know the real prefix for this attribute's
+ // value, which is needed to generate the completion choices below.
+ attrInfo.correctedPrefix = value;
+ } else {
+ attrInfo.needTag = '"';
+ }
+
+ if (currentUiNode != null) {
+ // look for an UI attribute matching the current attribute name
+ String attrName = attrInfo.name;
+ // remove any namespace prefix from the attribute name
+ int pos = attrName.indexOf(':');
+ if (pos >= 0) {
+ attrName = attrName.substring(pos + 1);
+ }
+
+ UiAttributeNode currAttrNode = null;
+ for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) {
+ if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) {
+ currAttrNode = attrNode;
+ break;
+ }
+ }
+
+ if (currAttrNode != null) {
+ choices = getAttributeValueChoices(currAttrNode, attrInfo, value);
+ }
+ }
+
+ if (choices == null) {
+ // fallback on the older descriptor-only based lookup.
+
+ // in order to properly handle the special case of the name attribute in
+ // the action tag, we need the grandparent of the action node, to know
+ // what type of actions we need.
+ // e.g. activity -> intent-filter -> action[@name]
+ String greatGrandParentName = null;
+ Node grandParent = currentNode.getParentNode();
+ if (grandParent != null) {
+ Node greatGrandParent = grandParent.getParentNode();
+ if (greatGrandParent != null) {
+ greatGrandParentName = greatGrandParent.getLocalName();
+ }
+ }
+
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ choices = data.getAttributeValues(parent, attrInfo.name, greatGrandParentName);
+ }
+ }
+ } else {
+ // Editing an attribute's name... Get attributes valid for the parent node.
+ if (currentUiNode != null) {
+ choices = currentUiNode.getAttributeDescriptors();
+ } else {
+ ElementDescriptor parentDesc = getDescriptor(parent);
+ if (parentDesc != null) {
+ choices = parentDesc.getAttributes();
+ }
+ }
+ }
+ return choices;
+ }
+
+ protected Object[] getAttributeValueChoices(UiAttributeNode currAttrNode, AttribInfo attrInfo,
+ String value) {
+ Object[] choices;
+ int pos;
+ choices = currAttrNode.getPossibleValues(value);
+ if (choices != null && currAttrNode instanceof UiResourceAttributeNode) {
+ attrInfo.skipEndTag = false;
+ }
+
+ if (currAttrNode instanceof UiFlagAttributeNode) {
+ // A "flag" can consist of several values separated by "or" (|).
+ // If the correct prefix contains such a pipe character, we change
+ // it so that only the currently edited value is completed.
+ pos = value.lastIndexOf('|');
+ if (pos >= 0) {
+ attrInfo.correctedPrefix = value = value.substring(pos + 1);
+ attrInfo.needTag = 0;
+ }
+
+ attrInfo.skipEndTag = false;
+ }
+
+ // Should we do suffix completion on dimension units etc?
+ choices = completeSuffix(choices, value, currAttrNode);
+
+ // Check to see if the user is attempting resource completion
+ AttributeDescriptor attributeDescriptor = currAttrNode.getDescriptor();
+ IAttributeInfo attributeInfo = attributeDescriptor.getAttributeInfo();
+ if (value.startsWith(PREFIX_RESOURCE_REF)
+ && !attributeInfo.getFormats().contains(Format.REFERENCE)) {
+ // Special case: If the attribute value looks like a reference to a
+ // resource, offer to complete it, since in many cases our metadata
+ // does not correctly state whether a resource value is allowed. We don't
+ // offer these for an empty completion context, but if the user has
+ // actually typed "@", in that case list resource matches.
+ // For example, for android:minHeight this makes completion on @dimen/
+ // possible.
+ choices = UiResourceAttributeNode.computeResourceStringMatches(
+ mEditor, attributeDescriptor, value);
+ attrInfo.skipEndTag = false;
+ } else if (value.startsWith(PREFIX_THEME_REF)
+ && !attributeInfo.getFormats().contains(Format.REFERENCE)) {
+ choices = UiResourceAttributeNode.computeResourceStringMatches(
+ mEditor, attributeDescriptor, value);
+ attrInfo.skipEndTag = false;
+ }
+
+ return choices;
+ }
+
+ /**
+ * Compute attribute values. Return true if the complete set of values was
+ * added, so addition descriptor information should not be added.
+ */
+ protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset,
+ String parentTagName, String attributeName, Node node, String wordPrefix,
+ boolean skipEndTag, int replaceLength) {
+ return false;
+ }
+
+ protected void computeTextValues(List<ICompletionProposal> proposals, int offset,
+ Node parentNode, Node currentNode, UiElementNode uiParent,
+ String wordPrefix) {
+
+ if (parentNode != null) {
+ // Examine the parent of the text node.
+ Object[] choices = getElementChoicesForTextNode(parentNode);
+ if (choices != null && choices.length > 0) {
+ ISourceViewer viewer = mEditor.getStructuredSourceViewer();
+ char needTag = computeElementNeedTag(viewer, offset, wordPrefix);
+
+ int replaceLength = 0;
+ addMatchingProposals(proposals, choices,
+ offset, parentNode, wordPrefix, needTag,
+ false /* isAttribute */,
+ false /*isNew*/,
+ false /*isComplete*/,
+ replaceLength);
+ }
+ }
+ }
+
+ /**
+ * Gets the choices when the user is editing an XML text node.
+ * <p/>
+ * This means the user is editing outside of any XML element or attribute.
+ * Simply return the list of XML elements that can be present there, based on the
+ * parent of the current node.
+ *
+ * @return An ElementDescriptor[] or null.
+ */
+ protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) {
+ ElementDescriptor[] choices = null;
+ String parent;
+ if (parentNode.getNodeType() == Node.ELEMENT_NODE) {
+ // We're editing a text node which parent is an element node. Limit
+ // content assist to elements valid for the parent.
+ parent = parentNode.getNodeName();
+ ElementDescriptor desc = getDescriptor(parent);
+ if (desc == null && parent.indexOf('.') != -1) {
+ // The parent is a custom view and we don't have metadata about its
+ // allowable children, so just assume any normal layout tag is
+ // legal
+ desc = mRootDescriptor;
+ }
+
+ if (desc != null) {
+ choices = sort(desc.getChildren());
+ }
+ } else if (parentNode.getNodeType() == Node.DOCUMENT_NODE) {
+ // We're editing a text node at the first level (i.e. root node).
+ // Limit content assist to the only valid root elements.
+ choices = sort(getRootDescriptor().getChildren());
+ }
+
+ return choices;
+ }
+
+ /**
+ * Given a list of choices, adds in any that match the current prefix into the
+ * proposals list.
+ * <p/>
+ * Choices is an object array. Items of the array can be:
+ * - ElementDescriptor: a possible element descriptor which XML name should be completed.
+ * - AttributeDescriptor: a possible attribute descriptor which XML name should be completed.
+ * - String: string values to display as-is to the user. Typically those are possible
+ * values for a given attribute.
+ * - Pair of Strings: the first value is the keyword to insert, and the second value
+ * is the tooltip/help for the value to be displayed in the documentation popup.
+ */
+ protected void addMatchingProposals(List<ICompletionProposal> proposals, Object[] choices,
+ int offset, Node currentNode, String wordPrefix, char needTag,
+ boolean isAttribute, boolean isNew, boolean skipEndTag, int replaceLength) {
+ if (choices == null) {
+ return;
+ }
+
+ Map<String, String> nsUriMap = new HashMap<String, String>();
+ boolean haveLayoutParams = false;
+
+ for (Object choice : choices) {
+ String keyword = null;
+ String nsPrefix = null;
+ String nsUri = null;
+ Image icon = null;
+ String tooltip = null;
+ if (choice instanceof ElementDescriptor) {
+ keyword = ((ElementDescriptor)choice).getXmlName();
+ icon = ((ElementDescriptor)choice).getGenericIcon();
+ // Tooltip computed lazily in {@link CompletionProposal}
+ } else if (choice instanceof TextValueDescriptor) {
+ continue; // Value nodes are not part of the completion choices
+ } else if (choice instanceof SeparatorAttributeDescriptor) {
+ continue; // not real attribute descriptors
+ } else if (choice instanceof AttributeDescriptor) {
+ keyword = ((AttributeDescriptor)choice).getXmlLocalName();
+ icon = ((AttributeDescriptor)choice).getGenericIcon();
+ // Tooltip computed lazily in {@link CompletionProposal}
+
+ // Get the namespace URI for the attribute. Note that some attributes
+ // do not have a namespace and thus return null here.
+ nsUri = ((AttributeDescriptor)choice).getNamespaceUri();
+ if (nsUri != null) {
+ nsPrefix = nsUriMap.get(nsUri);
+ if (nsPrefix == null) {
+ nsPrefix = XmlUtils.lookupNamespacePrefix(currentNode, nsUri, false);
+ nsUriMap.put(nsUri, nsPrefix);
+ }
+ }
+ if (nsPrefix != null) {
+ nsPrefix += ":"; //$NON-NLS-1$
+ }
+
+ } else if (choice instanceof String) {
+ keyword = (String) choice;
+ if (isAttribute) {
+ icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME);
+ }
+ } else if (choice instanceof Pair<?, ?>) {
+ @SuppressWarnings("unchecked")
+ Pair<String, String> pair = (Pair<String, String>) choice;
+ keyword = pair.getFirst();
+ tooltip = pair.getSecond();
+ if (isAttribute) {
+ icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME);
+ }
+ } else if (choice instanceof IType) {
+ IType type = (IType) choice;
+ keyword = type.getFullyQualifiedName();
+ icon = JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CUNIT);
+ } else {
+ continue; // discard unknown choice
+ }
+
+ String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword);
+
+ if (nameStartsWith(nsKeyword, wordPrefix, nsPrefix)) {
+ keyword = nsKeyword;
+ String endTag = ""; //$NON-NLS-1$
+ if (needTag != 0) {
+ if (needTag == '"') {
+ keyword = needTag + keyword;
+ endTag = String.valueOf(needTag);
+ } else if (needTag == '<') {
+ if (elementCanHaveChildren(choice)) {
+ endTag = String.format("></%1$s>", keyword); //$NON-NLS-1$
+ } else {
+ endTag = "/>"; //$NON-NLS-1$
+ }
+ keyword = needTag + keyword + ' ';
+ } else if (needTag == ' ') {
+ keyword = needTag + keyword;
+ }
+ } else if (!isAttribute && isNew) {
+ if (elementCanHaveChildren(choice)) {
+ endTag = String.format("></%1$s>", keyword); //$NON-NLS-1$
+ } else {
+ endTag = "/>"; //$NON-NLS-1$
+ }
+ keyword = keyword + ' ';
+ }
+
+ final String suffix;
+ int cursorPosition;
+ final String displayString;
+ if (choice instanceof AttributeDescriptor && isNew) {
+ // Special case for attributes: insert ="" stuff and locate caret inside ""
+ suffix = "=\"\""; //$NON-NLS-1$
+ cursorPosition = keyword.length() + suffix.length() - 1;
+ displayString = keyword + endTag; // don't include suffix;
+ } else {
+ suffix = endTag;
+ cursorPosition = keyword.length();
+ displayString = null;
+ }
+
+ if (skipEndTag) {
+ assert isAttribute;
+ cursorPosition++;
+ }
+
+ if (nsPrefix != null &&
+ keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())) {
+ haveLayoutParams = true;
+ }
+
+ // For attributes, automatically insert ns:attribute="" and place the cursor
+ // inside the quotes.
+ // Special case for attributes: insert ="" stuff and locate caret inside ""
+ proposals.add(new CompletionProposal(
+ this,
+ choice,
+ keyword + suffix, // String replacementString
+ offset - wordPrefix.length(), // int replacementOffset
+ wordPrefix.length() + replaceLength,// int replacementLength
+ cursorPosition, // cursorPosition
+ icon, // Image image
+ displayString, // displayString
+ null, // IContextInformation contextInformation
+ tooltip, // String additionalProposalInfo
+ nsPrefix,
+ nsUri
+ ));
+ }
+ }
+
+ if (wordPrefix.length() > 0 && haveLayoutParams
+ && !wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ // Sort layout parameters to the front if we automatically inserted some
+ // that you didn't request. For example, you typed "width" and we match both
+ // "width" and "layout_width" - should match layout_width.
+ String nsPrefix = nsUriMap.get(ANDROID_URI);
+ if (nsPrefix == null) {
+ nsPrefix = PREFIX_ANDROID;
+ } else {
+ nsPrefix += ':';
+ }
+ if (!(wordPrefix.startsWith(nsPrefix)
+ && wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()))) {
+ int nextLayoutIndex = 0;
+ for (int i = 0, n = proposals.size(); i < n; i++) {
+ ICompletionProposal proposal = proposals.get(i);
+ String keyword = proposal.getDisplayString();
+ if (keyword.startsWith(nsPrefix) &&
+ keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())
+ && i != nextLayoutIndex) {
+ // Swap to front
+ ICompletionProposal temp = proposals.get(nextLayoutIndex);
+ proposals.set(nextLayoutIndex, proposal);
+ proposals.set(i, temp);
+ nextLayoutIndex++;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if the given word starts with the given prefix. The comparison is not
+ * case sensitive.
+ *
+ * @param word the word to test
+ * @param prefix the prefix the word should start with
+ * @return true if the given word starts with the given prefix
+ */
+ protected static boolean startsWith(String word, String prefix) {
+ int prefixLength = prefix.length();
+ int wordLength = word.length();
+ if (wordLength < prefixLength) {
+ return false;
+ }
+
+ for (int i = 0; i < prefixLength; i++) {
+ if (Character.toLowerCase(prefix.charAt(i))
+ != Character.toLowerCase(word.charAt(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** @return the editor associated with this content assist */
+ AndroidXmlEditor getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * This method performs a prefix match for the given word and prefix, with a couple of
+ * Android code completion specific twists:
+ * <ol>
+ * <li> The match is not case sensitive, so {word="fOo",prefix="FoO"} is a match.
+ * <li>If the word to be matched has a namespace prefix, the typed prefix doesn't have
+ * to match it. So {word="android:foo", prefix="foo"} is a match.
+ * <li>If the attribute name part starts with "layout_" it can be omitted. So
+ * {word="android:layout_marginTop",prefix="margin"} is a match, as is
+ * {word="android:layout_marginTop",prefix="android:margin"}.
+ * </ol>
+ *
+ * @param word the full word to be matched, including namespace if any
+ * @param prefix the prefix to check
+ * @param nsPrefix the namespace prefix (android: or local definition of android
+ * namespace prefix)
+ * @return true if the prefix matches for code completion
+ */
+ protected static boolean nameStartsWith(String word, String prefix, String nsPrefix) {
+ if (nsPrefix == null) {
+ nsPrefix = ""; //$NON-NLS-1$
+ }
+
+ int wordStart = nsPrefix.length();
+ int prefixStart = 0;
+
+ if (startsWith(prefix, nsPrefix)) {
+ // Already matches up through the namespace prefix:
+ prefixStart = wordStart;
+ } else if (startsWith(nsPrefix, prefix)) {
+ return true;
+ }
+
+ int prefixLength = prefix.length();
+ int wordLength = word.length();
+
+ if (wordLength - wordStart < prefixLength - prefixStart) {
+ return false;
+ }
+
+ boolean matches = true;
+ for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) {
+ char c1 = Character.toLowerCase(prefix.charAt(i));
+ char c2 = Character.toLowerCase(word.charAt(j));
+ if (c1 != c2) {
+ matches = false;
+ break;
+ }
+ }
+
+ if (!matches && word.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, wordStart)
+ && !prefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, prefixStart)) {
+ wordStart += ATTR_LAYOUT_RESOURCE_PREFIX.length();
+
+ if (wordLength - wordStart < prefixLength - prefixStart) {
+ return false;
+ }
+
+ for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) {
+ char c1 = Character.toLowerCase(prefix.charAt(i));
+ char c2 = Character.toLowerCase(word.charAt(j));
+ if (c1 != c2) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return matches;
+ }
+
+ /**
+ * Indicates whether this descriptor describes an element that can potentially
+ * have children (either sub-elements or text value). If an element can have children,
+ * we want to explicitly write an opening and a separate closing tag.
+ * <p/>
+ * Elements can have children if the descriptor has children element descriptors
+ * or if one of the attributes is a TextValueDescriptor.
+ *
+ * @param descriptor An ElementDescriptor or an AttributeDescriptor
+ * @return True if the descriptor is an ElementDescriptor that can have children or a text
+ * value
+ */
+ private boolean elementCanHaveChildren(Object descriptor) {
+ if (descriptor instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) descriptor;
+ if (desc.hasChildren()) {
+ return true;
+ }
+ for (AttributeDescriptor attrDesc : desc.getAttributes()) {
+ if (attrDesc instanceof TextValueDescriptor) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the element descriptor matching a given XML node name or null if it can't be
+ * found.
+ * <p/>
+ * This is simplistic; ideally we should consider the parent's chain to make sure we
+ * can differentiate between different hierarchy trees. Right now the first match found
+ * is returned.
+ */
+ private ElementDescriptor getDescriptor(String nodeName) {
+ return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */);
+ }
+
+ @Override
+ public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
+ return null;
+ }
+
+ /**
+ * Returns the characters which when entered by the user should
+ * automatically trigger the presentation of possible completions.
+ *
+ * In our case, we auto-activate on opening tags and attributes namespace.
+ *
+ * @return the auto activation characters for completion proposal or <code>null</code>
+ * if no auto activation is desired
+ */
+ @Override
+ public char[] getCompletionProposalAutoActivationCharacters() {
+ return new char[]{ '<', ':', '=' };
+ }
+
+ @Override
+ public char[] getContextInformationAutoActivationCharacters() {
+ return null;
+ }
+
+ @Override
+ public IContextInformationValidator getContextInformationValidator() {
+ return null;
+ }
+
+ @Override
+ public String getErrorMessage() {
+ return null;
+ }
+
+ /**
+ * Heuristically extracts the prefix used for determining template relevance
+ * from the viewer's document. The default implementation returns the String from
+ * offset backwards that forms a potential XML element name, attribute name or
+ * attribute value.
+ *
+ * The part were we access the document was extracted from
+ * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs.
+ *
+ * @param viewer the viewer
+ * @param offset offset into document
+ * @return the prefix to consider
+ */
+ protected String extractElementPrefix(ITextViewer viewer, int offset) {
+ int i = offset;
+ IDocument document = viewer.getDocument();
+ if (i > document.getLength()) return ""; //$NON-NLS-1$
+
+ try {
+ for (; i > 0; --i) {
+ char ch = document.getChar(i - 1);
+
+ // We want all characters that can form a valid:
+ // - element name, e.g. anything that is a valid Java class/variable literal.
+ // - attribute name, including : for the namespace
+ // - attribute value.
+ // Before we were inclusive and that made the code fragile. So now we're
+ // going to be exclusive: take everything till we get one of:
+ // - any form of whitespace
+ // - any xml separator, e.g. < > ' " and =
+ if (Character.isWhitespace(ch) ||
+ ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') {
+ break;
+ }
+ }
+
+ return document.get(i, offset - i);
+ } catch (BadLocationException e) {
+ return ""; //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Extracts the character at the given offset.
+ * Returns 0 if the offset is invalid.
+ */
+ protected char extractChar(ITextViewer viewer, int offset) {
+ IDocument document = viewer.getDocument();
+ if (offset > document.getLength()) return 0;
+
+ try {
+ return document.getChar(offset);
+ } catch (BadLocationException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Search forward and find the first non-space character and return it. Returns 0 if no
+ * such character was found.
+ */
+ private char nextNonspaceChar(ITextViewer viewer, int offset) {
+ IDocument document = viewer.getDocument();
+ int length = document.getLength();
+ for (; offset < length; offset++) {
+ try {
+ char c = document.getChar(offset);
+ if (!Character.isWhitespace(c)) {
+ return c;
+ }
+ } catch (BadLocationException e) {
+ return 0;
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Information about the current edit of an attribute as reported by parseAttributeInfo.
+ */
+ protected static class AttribInfo {
+ public AttribInfo() {
+ }
+
+ /** True if the cursor is located in an attribute's value, false if in an attribute name */
+ public boolean isInValue = false;
+ /** The attribute name. Null when not set. */
+ public String name = null;
+ /** The attribute value top the left of the cursor. Null when not set. The value
+ * *may* start with a quote (' or "), in which case we know we don't need to quote
+ * the string for the user */
+ public String valuePrefix = null;
+ /** String typed by the user so far (i.e. right before requesting code completion),
+ * which will be corrected if we find a possible completion for an attribute value.
+ * See the long comment in getChoicesForAttribute(). */
+ public String correctedPrefix = null;
+ /** Non-zero if an attribute value need a start/end tag (i.e. quotes or brackets) */
+ public char needTag = 0;
+ /** Number of characters to replace after the prefix */
+ public int replaceLength = 0;
+ /** Should the cursor advance through the end tag when inserted? */
+ public boolean skipEndTag = false;
+ }
+
+ /**
+ * Try to guess if the cursor is editing an element's name or an attribute following an
+ * element. If it's an attribute, try to find if an attribute name is being defined or
+ * its value.
+ * <br/>
+ * This is currently *only* called when we know the cursor is after a complete element
+ * tag name, so it should never return null.
+ * <br/>
+ * Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags
+ * <br/>
+ * @return An AttribInfo describing which attribute is being edited or null if the cursor is
+ * not editing an attribute (in which case it must be an element's name).
+ */
+ private AttribInfo parseAttributeInfo(ITextViewer viewer, int offset, int prefixStartOffset) {
+ AttribInfo info = new AttribInfo();
+ int originalOffset = offset;
+
+ IDocument document = viewer.getDocument();
+ int n = document.getLength();
+ if (offset <= n) {
+ try {
+ // Look to the right to make sure we aren't sitting on the boundary of the
+ // beginning of a new element with whitespace before it
+ if (offset < n && document.getChar(offset) == '<') {
+ return null;
+ }
+
+ n = offset;
+ for (;offset > 0; --offset) {
+ char ch = document.getChar(offset - 1);
+ if (ch == '>') break;
+ if (ch == '<') break;
+ }
+
+ // text will contain the full string of the current element,
+ // i.e. whatever is after the "<" to the current cursor
+ String text = document.get(offset, n - offset);
+
+ // Normalize whitespace to single spaces
+ text = sWhitespace.matcher(text).replaceAll(" "); //$NON-NLS-1$
+
+ // Remove the leading element name. By spec, it must be after the < without
+ // any whitespace. If there's nothing left, no attribute has been defined yet.
+ // Be sure to keep any whitespace after the initial word if any, as it matters.
+ text = sFirstElementWord.matcher(text).replaceFirst(""); //$NON-NLS-1$
+
+ // There MUST be space after the element name. If not, the cursor is still
+ // defining the element name.
+ if (!text.startsWith(" ")) { //$NON-NLS-1$
+ return null;
+ }
+
+ // Remove full attributes:
+ // Syntax:
+ // name = "..." quoted string with all but < and "
+ // or:
+ // name = '...' quoted string with all but < and '
+ String temp;
+ do {
+ temp = text;
+ text = sFirstAttribute.matcher(temp).replaceFirst(""); //$NON-NLS-1$
+ } while(!temp.equals(text));
+
+ IRegion lineInfo = document.getLineInformationOfOffset(originalOffset);
+ int lineStart = lineInfo.getOffset();
+ String line = document.get(lineStart, lineInfo.getLength());
+ int cursorColumn = originalOffset - lineStart;
+ int prefixLength = originalOffset - prefixStartOffset;
+
+ // Now we're left with 3 cases:
+ // - nothing: either there is no attribute definition or the cursor located after
+ // a completed attribute definition.
+ // - a string with no =: the user is writing an attribute name. This case can be
+ // merged with the previous one.
+ // - string with an = sign, optionally followed by a quote (' or "): the user is
+ // writing the value of the attribute.
+ int posEqual = text.indexOf('=');
+ if (posEqual == -1) {
+ info.isInValue = false;
+ info.name = text.trim();
+
+ // info.name is currently just the prefix of the attribute name.
+ // Look at the text buffer to find the complete name (since we need
+ // to know its bounds in order to replace it when a different attribute
+ // that matches this prefix is chosen)
+ int nameStart = cursorColumn;
+ for (int nameEnd = nameStart; nameEnd < line.length(); nameEnd++) {
+ char c = line.charAt(nameEnd);
+ if (!(Character.isLetter(c) || c == ':' || c == '_')) {
+ String nameSuffix = line.substring(nameStart, nameEnd);
+ info.name = text.trim() + nameSuffix;
+ break;
+ }
+ }
+
+ info.replaceLength = info.name.length() - prefixLength;
+
+ if (info.name.length() == 0 && originalOffset > 0) {
+ // Ensure that attribute names are properly separated
+ char prevChar = extractChar(viewer, originalOffset - 1);
+ if (prevChar == '"' || prevChar == '\'') {
+ // Ensure that the attribute is properly separated from the
+ // previous element
+ info.needTag = ' ';
+ }
+ }
+ info.skipEndTag = false;
+ } else {
+ info.isInValue = true;
+ info.name = text.substring(0, posEqual).trim();
+ info.valuePrefix = text.substring(posEqual + 1);
+
+ char quoteChar = '"'; // Does " or ' surround the XML value?
+ for (int i = posEqual + 1; i < text.length(); i++) {
+ if (!Character.isWhitespace(text.charAt(i))) {
+ quoteChar = text.charAt(i);
+ break;
+ }
+ }
+
+ // Must compute the complete value
+ int valueStart = cursorColumn;
+ int valueEnd = valueStart;
+ for (; valueEnd < line.length(); valueEnd++) {
+ char c = line.charAt(valueEnd);
+ if (c == quoteChar) {
+ // Make sure this isn't the *opening* quote of the value,
+ // which is the case if we invoke code completion with the
+ // caret between the = and the opening quote; in that case
+ // we consider it value completion, and offer items including
+ // the quotes, but we shouldn't bail here thinking we have found
+ // the end of the value.
+ // Look backwards to make sure we find another " before
+ // we find a =
+ boolean isFirst = false;
+ for (int j = valueEnd - 1; j >= 0; j--) {
+ char pc = line.charAt(j);
+ if (pc == '=') {
+ isFirst = true;
+ break;
+ } else if (pc == quoteChar) {
+ valueStart = j;
+ break;
+ }
+ }
+ if (!isFirst) {
+ info.skipEndTag = true;
+ break;
+ }
+ }
+ }
+ int valueEndOffset = valueEnd + lineStart;
+ info.replaceLength = valueEndOffset - (prefixStartOffset + prefixLength);
+ // Is the caret to the left of the value quote? If so, include it in
+ // the replace length.
+ int valueStartOffset = valueStart + lineStart;
+ if (valueStartOffset == prefixStartOffset && valueEnd > valueStart) {
+ info.replaceLength++;
+ }
+ }
+ return info;
+ } catch (BadLocationException e) {
+ // pass
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the root descriptor id to use */
+ protected int getRootDescriptorId() {
+ return mDescriptorId;
+ }
+
+ /**
+ * Computes (if needed) and returns the root descriptor.
+ */
+ protected ElementDescriptor getRootDescriptor() {
+ if (mRootDescriptor == null) {
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ IDescriptorProvider descriptorProvider =
+ data.getDescriptorProvider(getRootDescriptorId());
+
+ if (descriptorProvider != null) {
+ mRootDescriptor = new ElementDescriptor("", //$NON-NLS-1$
+ descriptorProvider.getRootElementDescriptors());
+ }
+ }
+ }
+
+ return mRootDescriptor;
+ }
+
+ /**
+ * Fixed list of dimension units, along with user documentation, for use by
+ * {@link #completeSuffix}.
+ */
+ private static final String[] sDimensionUnits = new String[] {
+ UNIT_DP,
+ "<b>Density-independent Pixels</b> - an abstract unit that is based on the physical "
+ + "density of the screen.",
+
+ UNIT_SP,
+ "<b>Scale-independent Pixels</b> - this is like the dp unit, but it is also scaled by "
+ + "the user's font size preference.",
+
+ UNIT_PT,
+ "<b>Points</b> - 1/72 of an inch based on the physical size of the screen.",
+
+ UNIT_MM,
+ "<b>Millimeters</b> - based on the physical size of the screen.",
+
+ UNIT_IN,
+ "<b>Inches</b> - based on the physical size of the screen.",
+
+ UNIT_PX,
+ "<b>Pixels</b> - corresponds to actual pixels on the screen. Not recommended.",
+ };
+
+ /**
+ * Fixed list of fractional units, along with user documentation, for use by
+ * {@link #completeSuffix}
+ */
+ private static final String[] sFractionUnits = new String[] {
+ "%", //$NON-NLS-1$
+ "<b>Fraction</b> - a percentage of the base size",
+
+ "%p", //$NON-NLS-1$
+ "<b>Fraction</b> - a percentage relative to parent container",
+ };
+
+ /**
+ * Completes suffixes for applicable types (like dimensions and fractions) such that
+ * after a dimension number you get completion on unit types like "px".
+ */
+ private Object[] completeSuffix(Object[] choices, String value, UiAttributeNode currAttrNode) {
+ IAttributeInfo attributeInfo = currAttrNode.getDescriptor().getAttributeInfo();
+ EnumSet<Format> formats = attributeInfo.getFormats();
+ List<Object> suffixes = new ArrayList<Object>();
+
+ if (value.length() > 0 && Character.isDigit(value.charAt(0))) {
+ boolean hasDimension = formats.contains(Format.DIMENSION);
+ boolean hasFraction = formats.contains(Format.FRACTION);
+
+ if (hasDimension || hasFraction) {
+ // Split up the value into a numeric part (the prefix) and the
+ // unit part (the suffix)
+ int suffixBegin = 0;
+ for (; suffixBegin < value.length(); suffixBegin++) {
+ if (!Character.isDigit(value.charAt(suffixBegin))) {
+ break;
+ }
+ }
+ String number = value.substring(0, suffixBegin);
+ String suffix = value.substring(suffixBegin);
+
+ // Add in the matching dimension and/or fraction units, if any
+ if (hasDimension) {
+ // Each item has two entries in the array of strings: the first odd numbered
+ // ones are the unit names and the second even numbered ones are the
+ // corresponding descriptions.
+ for (int i = 0; i < sDimensionUnits.length; i += 2) {
+ String unit = sDimensionUnits[i];
+ if (startsWith(unit, suffix)) {
+ String description = sDimensionUnits[i + 1];
+ suffixes.add(Pair.of(number + unit, description));
+ }
+ }
+
+ // Allow "dip" completion but don't offer it ("dp" is preferred)
+ if (startsWith(suffix, "di") || startsWith(suffix, "dip")) { //$NON-NLS-1$ //$NON-NLS-2$
+ suffixes.add(Pair.of(number + "dip", "Alternative name for \"dp\"")); //$NON-NLS-1$
+ }
+ }
+ if (hasFraction) {
+ for (int i = 0; i < sFractionUnits.length; i += 2) {
+ String unit = sFractionUnits[i];
+ if (startsWith(unit, suffix)) {
+ String description = sFractionUnits[i + 1];
+ suffixes.add(Pair.of(number + unit, description));
+ }
+ }
+ }
+ }
+ }
+
+ boolean hasFlag = formats.contains(Format.FLAG);
+ if (hasFlag) {
+ boolean isDone = false;
+ String[] flagValues = attributeInfo.getFlagValues();
+ for (String flagValue : flagValues) {
+ if (flagValue.equals(value)) {
+ isDone = true;
+ break;
+ }
+ }
+ if (isDone) {
+ // Add in all the new values with a separator of |
+ String currentValue = currAttrNode.getCurrentValue();
+ for (String flagValue : flagValues) {
+ if (currentValue == null || !currentValue.contains(flagValue)) {
+ suffixes.add(value + '|' + flagValue);
+ }
+ }
+ }
+ }
+
+ if (suffixes.size() > 0) {
+ // Merge previously added choices (from attribute enums etc) with the new matches
+ List<Object> all = new ArrayList<Object>();
+ if (choices != null) {
+ for (Object s : choices) {
+ all.add(s);
+ }
+ }
+ all.addAll(suffixes);
+ choices = all.toArray();
+ }
+
+ return choices;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java
new file mode 100644
index 000000000..87164871f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.eclipse.wst.xml.ui.internal.doubleclick.XMLDoubleClickStrategy;
+
+/**
+ * Custom version of {@link XMLDoubleClickStrategy} which is smarter about
+ * selecting portions of resource references, etc.
+ */
+@SuppressWarnings("restriction") // XML API
+public class AndroidDoubleClickStrategy extends XMLDoubleClickStrategy {
+ /**
+ * Creates a new {@linkplain AndroidDoubleClickStrategy}
+ */
+ public AndroidDoubleClickStrategy() {
+ }
+
+ @Override
+ protected void processElementDoubleClicked() {
+ // Special case: if you click on the local name portion of an attribute pair,
+ // select only the local name. For example, if you click anywhere in the "text" region
+ // of "android:text", select just the "text" portion.
+ if (fTextRegion.getType() == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) {
+ String regionText = fStructuredDocumentRegion.getText(fTextRegion);
+ int cursor = fCaretPosition - fStructuredDocumentRegion.getStartOffset(fTextRegion);
+ int ns = regionText.indexOf(':');
+ if (cursor > ns) {
+ int start = ns + 1;
+ fTextViewer.setSelectedRange(fStructuredDocumentRegion.getStartOffset(fTextRegion)
+ + start, fTextRegion.getTextLength() - start);
+ return;
+ }
+ }
+
+ super.processElementDoubleClicked();
+ }
+
+ @Override
+ protected Point getWord(String string, int cursor) {
+ if (string == null) {
+ return null;
+ }
+
+ // Default implementation will strip off the surrounding quotes etc:
+ Point position = super.getWord(string, cursor);
+
+ assert cursor >= position.x && cursor <= position.y;
+
+ // Special case: when you click on a resource identifier name, only select the
+ // name portion
+ if (string.startsWith(PREFIX_RESOURCE_REF, position.x) ||
+ string.startsWith(PREFIX_THEME_REF, position.x)) {
+ int nameStart = string.indexOf('/', position.x + 1);
+ if (nameStart != -1 && nameStart < cursor) {
+ position.x = nameStart + 1;
+ return position;
+ }
+ }
+
+ // Special case: when you have a dotted name, such as com.android.tools.MyClass,
+ // and you click on the last part, select only that part
+ int lastDot = string.lastIndexOf('.', cursor);
+ if (lastDot >= position.x && lastDot < position.y - 1) {
+ int next = string.indexOf('.', cursor);
+ if (next == -1 || next > position.y) {
+ position.x = lastDot + 1;
+ }
+ }
+
+ return position;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidOutlineConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidOutlineConfiguration.java
new file mode 100644
index 000000000..6061d929c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidOutlineConfiguration.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.wst.xml.ui.views.contentoutline.XMLContentOutlineConfiguration;
+
+/**
+ * Custom version of {@link XMLContentOutlineConfiguration} which adds in icons and
+ * details such as id or name, to the labels.
+ */
+public class AndroidOutlineConfiguration extends XMLContentOutlineConfiguration {
+ /** Constructs a new {@link AndroidOutlineConfiguration} */
+ public AndroidOutlineConfiguration() {
+ }
+
+ @Override
+ public ILabelProvider getLabelProvider(TreeViewer viewer) {
+ return new OutlineLabelProvider();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidQuickOutlineConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidQuickOutlineConfiguration.java
new file mode 100644
index 000000000..0a8e9dc12
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidQuickOutlineConfiguration.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.wst.xml.ui.internal.quickoutline.XMLQuickOutlineConfiguration;
+
+/**
+ * Custom version of {@link XMLQuickOutlineConfiguration} which adds in icons and
+ * details such as id or name, to the labels.
+ */
+public class AndroidQuickOutlineConfiguration extends XMLQuickOutlineConfiguration {
+ /** Constructs a new {@link AndroidQuickOutlineConfiguration} */
+ public AndroidQuickOutlineConfiguration() {
+ }
+
+ @Override
+ public ILabelProvider getLabelProvider() {
+ return new OutlineLabelProvider();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java
new file mode 100644
index 000000000..b2c10ec3b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+
+import com.android.ide.eclipse.adt.internal.editors.formatting.AndroidXmlFormatter;
+import com.android.ide.eclipse.adt.internal.editors.formatting.AndroidXmlFormattingStrategy;
+
+import org.eclipse.jface.text.DefaultAutoIndentStrategy;
+import org.eclipse.jface.text.IAutoEditStrategy;
+import org.eclipse.jface.text.ITextHover;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+import org.eclipse.jface.text.formatter.IContentFormatter;
+import org.eclipse.jface.text.formatter.MultiPassContentFormatter;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.wst.sse.core.text.IStructuredPartitions;
+import org.eclipse.wst.xml.core.text.IXMLPartitions;
+import org.eclipse.wst.xml.ui.StructuredTextViewerConfigurationXML;
+import org.eclipse.wst.xml.ui.internal.contentassist.XMLContentAssistProcessor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base Source Viewer Configuration for Android resources.
+ */
+@SuppressWarnings({"restriction", "deprecation"}) // XMLContentAssistProcessor etc
+public abstract class AndroidSourceViewerConfig extends StructuredTextViewerConfigurationXML {
+
+ public AndroidSourceViewerConfig() {
+ super();
+ }
+
+ @Override
+ public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {
+ return super.getContentAssistant(sourceViewer);
+ }
+
+ /**
+ * Returns the IContentAssistProcessor that
+ * {@link #getContentAssistProcessors(ISourceViewer, String)} should use
+ * for XML and default/unknown partitions.
+ *
+ * @return An {@link IContentAssistProcessor} or null.
+ */
+ public abstract IContentAssistProcessor getAndroidContentAssistProcessor(
+ ISourceViewer sourceViewer,
+ String partitionType);
+
+ /**
+ * Returns the content assist processors that will be used for content
+ * assist in the given source viewer and for the given partition type.
+ *
+ * @param sourceViewer the source viewer to be configured by this
+ * configuration
+ * @param partitionType the partition type for which the content assist
+ * processors are applicable
+ * @return IContentAssistProcessors or null if should not be supported
+ */
+ @Override
+ protected IContentAssistProcessor[] getContentAssistProcessors(
+ ISourceViewer sourceViewer, String partitionType) {
+ ArrayList<IContentAssistProcessor> processors = new ArrayList<IContentAssistProcessor>();
+ if (partitionType == IStructuredPartitions.UNKNOWN_PARTITION ||
+ partitionType == IStructuredPartitions.DEFAULT_PARTITION ||
+ partitionType == IXMLPartitions.XML_DEFAULT) {
+
+ IContentAssistProcessor processor =
+ getAndroidContentAssistProcessor(sourceViewer, partitionType);
+
+ if (processor != null) {
+ processors.add(processor);
+ }
+ }
+
+ IContentAssistProcessor[] others = super.getContentAssistProcessors(sourceViewer,
+ partitionType);
+ if (others != null && others.length > 0) {
+ for (IContentAssistProcessor p : others) {
+ // Builtin Eclipse WTP code completion assistant? If so,
+ // wrap it with our own filter which hides some unwanted completions.
+ if (p instanceof XMLContentAssistProcessor
+ // On Eclipse 3.7, XMLContentAssistProcessor is no longer used,
+ // and instead org.eclipse.wst.xml.ui.internal.contentassist.
+ // XMLStructuredContentAssistProcessor is used - which isn't available
+ // at compile time in 3.5.
+ || p.getClass().getSimpleName().equals(
+ "XMLStructuredContentAssistProcessor")) { //$NON-NLS-1$
+ processors.add(new FilteringContentAssistProcessor(p));
+ } else {
+ processors.add(p);
+ }
+ }
+ }
+
+ if (processors.size() > 0) {
+ return processors.toArray(new IContentAssistProcessor[processors.size()]);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) {
+ // TODO text hover for android xml
+ return super.getTextHover(sourceViewer, contentType);
+ }
+
+ @Override
+ public IAutoEditStrategy[] getAutoEditStrategies(
+ ISourceViewer sourceViewer, String contentType) {
+ IAutoEditStrategy[] strategies = super.getAutoEditStrategies(sourceViewer, contentType);
+ List<IAutoEditStrategy> s = new ArrayList<IAutoEditStrategy>(strategies.length + 1);
+ s.add(new AndroidXmlAutoEditStrategy());
+
+ // Add other registered strategies, except the builtin indentation strategy which is
+ // now handled by the above AndroidXmlAutoEditStrategy
+ for (IAutoEditStrategy strategy : strategies) {
+ if (strategy instanceof DefaultAutoIndentStrategy) {
+ continue;
+ }
+ s.add(strategy);
+ }
+
+ return s.toArray(new IAutoEditStrategy[s.size()]);
+ }
+
+ @Override
+ public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) {
+ IContentFormatter formatter = super.getContentFormatter(sourceViewer);
+
+ if (formatter instanceof MultiPassContentFormatter) {
+ ((MultiPassContentFormatter) formatter).setMasterStrategy(
+ new AndroidXmlFormattingStrategy());
+ return formatter;
+ } else {
+ return new AndroidXmlFormatter();
+ }
+ }
+
+ @Override
+ protected Map<String, ?> getHyperlinkDetectorTargets(final ISourceViewer sourceViewer) {
+ @SuppressWarnings("unchecked")
+ Map<String, ?> targets = super.getHyperlinkDetectorTargets(sourceViewer);
+ // If we want to look up more context in our HyperlinkDetector via the
+ // getAdapter method, we should place an IAdaptable object into the map here.
+ targets.put("com.android.ide.eclipse.xmlCode", null); //$NON-NLS-1$
+ return targets;
+ }
+
+ /**
+ * A delegating {@link IContentAssistProcessor} whose purpose is to filter out some
+ * default Eclipse XML completions which are distracting in Android XML files
+ */
+ private static class FilteringContentAssistProcessor implements IContentAssistProcessor {
+ private IContentAssistProcessor mDelegate;
+
+ public FilteringContentAssistProcessor(IContentAssistProcessor delegate) {
+ super();
+ mDelegate = delegate;
+ }
+
+ @Override
+ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
+ ICompletionProposal[] result = mDelegate.computeCompletionProposals(viewer, offset);
+ if (result == null) {
+ return null;
+ }
+
+ List<ICompletionProposal> proposals =
+ new ArrayList<ICompletionProposal>(result.length);
+ for (ICompletionProposal proposal : result) {
+ String replacement = proposal.getDisplayString();
+ if (replacement.charAt(0) == '"' &&
+ replacement.charAt(replacement.length() - 1) == '"') {
+ // Filter out attribute values. In Android XML files (where there is no DTD
+ // etc) the default Eclipse XML code completion simply provides the
+ // existing value as a completion. This is often misleading, since if you
+ // for example have a typo, completion will show your current (wrong)
+ // value as a valid completion.
+ } else if (replacement.contains("Namespace") //$NON-NLS-1$
+ || replacement.startsWith("XSL ") //$NON-NLS-1$
+ || replacement.contains("Schema")) { //$NON-NLS-1$
+ // Eclipse adds in a number of namespace and schema related completions which
+ // are not usually applicable in our files.
+ } else {
+ proposals.add(proposal);
+ }
+ }
+
+ if (proposals.size() == result.length) {
+ return result;
+ } else {
+ return proposals.toArray(new ICompletionProposal[proposals.size()]);
+ }
+ }
+
+ @Override
+ public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
+ return mDelegate.computeContextInformation(viewer, offset);
+ }
+
+ @Override
+ public char[] getCompletionProposalAutoActivationCharacters() {
+ return mDelegate.getCompletionProposalAutoActivationCharacters();
+ }
+
+ @Override
+ public char[] getContextInformationAutoActivationCharacters() {
+ return mDelegate.getContextInformationAutoActivationCharacters();
+ }
+
+ @Override
+ public IContextInformationValidator getContextInformationValidator() {
+ return mDelegate.getContextInformationValidator();
+ }
+
+ @Override
+ public String getErrorMessage() {
+ return mDelegate.getErrorMessage();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java
new file mode 100644
index 000000000..2e60df5bf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java
@@ -0,0 +1,591 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.internal.filebuffers.SynchronizableDocument;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.jface.text.DocumentRewriteSession;
+import org.eclipse.jface.text.DocumentRewriteSessionType;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentExtension4;
+import org.eclipse.jface.text.IDocumentListener;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
+import org.eclipse.ui.editors.text.TextEditor;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IHyperlinkListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.MultiPageEditorPart;
+import org.eclipse.ui.part.WorkbenchPart;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Multi-page form editor for Android text files.
+ * <p/>
+ * It is designed to work with a {@link TextEditor} that will display a text file.
+ * <br/>
+ * Derived classes must implement createFormPages to create the forms before the
+ * source editor. This can be a no-op if desired.
+ */
+@SuppressWarnings("restriction")
+public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener {
+
+ /** Preference name for the current page of this file */
+ private static final String PREF_CURRENT_PAGE = "_current_page";
+
+ /** Id string used to create the Android SDK browser */
+ private static String BROWSER_ID = "android"; //$NON-NLS-1$
+
+ /** Page id of the XML source editor, used for switching tabs programmatically */
+ public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
+
+ /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
+ public static final int TEXT_WIDTH_HINT = 50;
+
+ /** Page index of the text editor (always the last page) */
+ private int mTextPageIndex;
+
+ /** The text editor */
+ private TextEditor mTextEditor;
+
+ /** flag set during page creation */
+ private boolean mIsCreatingPage = false;
+
+ private IDocument mDocument;
+
+ /**
+ * Creates a form editor.
+ */
+ public AndroidTextEditor() {
+ super();
+ }
+
+ // ---- Abstract Methods ----
+
+ /**
+ * Creates the various form pages.
+ * <p/>
+ * Derived classes must implement this to add their own specific tabs.
+ */
+ abstract protected void createFormPages();
+
+ /**
+ * Called by the base class {@link AndroidTextEditor} once all pages (custom form pages
+ * as well as text editor page) have been created. This give a chance to deriving
+ * classes to adjust behavior once the text page has been created.
+ */
+ protected void postCreatePages() {
+ // Nothing in the base class.
+ }
+
+ /**
+ * Subclasses should override this method to process the new text model.
+ * This is called after the document has been edited.
+ *
+ * The base implementation is empty.
+ *
+ * @param event Specification of changes applied to document.
+ */
+ protected void onDocumentChanged(DocumentEvent event) {
+ // pass
+ }
+
+ // ---- Base Class Overrides, Interfaces Implemented ----
+
+ /**
+ * Creates the pages of the multi-page editor.
+ */
+ @Override
+ protected void addPages() {
+ createAndroidPages();
+ selectDefaultPage(null /* defaultPageId */);
+ }
+
+ /**
+ * Creates the page for the Android Editors
+ */
+ protected void createAndroidPages() {
+ mIsCreatingPage = true;
+ createFormPages();
+ createTextEditor();
+ createUndoRedoActions();
+ postCreatePages();
+ mIsCreatingPage = false;
+ }
+
+ /**
+ * Returns whether the editor is currently creating its pages.
+ */
+ public boolean isCreatingPages() {
+ return mIsCreatingPage;
+ }
+
+ /**
+ * Creates undo redo actions for the editor site (so that it works for any page of this
+ * multi-page editor) by re-using the actions defined by the {@link TextEditor}
+ * (aka the XML text editor.)
+ */
+ private void createUndoRedoActions() {
+ IActionBars bars = getEditorSite().getActionBars();
+ if (bars != null) {
+ IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
+ bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
+
+ action = mTextEditor.getAction(ActionFactory.REDO.getId());
+ bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
+
+ bars.updateActionBars();
+ }
+ }
+
+ /**
+ * Selects the default active page.
+ * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
+ * find the default page in the properties of the {@link IResource} object being edited.
+ */
+ protected void selectDefaultPage(String defaultPageId) {
+ if (defaultPageId == null) {
+ if (getEditorInput() instanceof IFileEditorInput) {
+ IFile file = ((IFileEditorInput) getEditorInput()).getFile();
+
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ getClass().getSimpleName() + PREF_CURRENT_PAGE);
+ String pageId;
+ try {
+ pageId = file.getPersistentProperty(qname);
+ if (pageId != null) {
+ defaultPageId = pageId;
+ }
+ } catch (CoreException e) {
+ // ignored
+ }
+ }
+ }
+
+ if (defaultPageId != null) {
+ try {
+ setActivePage(Integer.parseInt(defaultPageId));
+ } catch (Exception e) {
+ // We can get NumberFormatException from parseInt but also
+ // AssertionError from setActivePage when the index is out of bounds.
+ // Generally speaking we just want to ignore any exception and fall back on the
+ // first page rather than crash the editor load. Logging the error is enough.
+ AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
+ }
+ }
+ }
+
+ /**
+ * Removes all the pages from the editor.
+ */
+ protected void removePages() {
+ int count = getPageCount();
+ for (int i = count - 1 ; i >= 0 ; i--) {
+ removePage(i);
+ }
+ }
+
+ /**
+ * Overrides the parent's setActivePage to be able to switch to the xml editor.
+ *
+ * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
+ * This is needed because the editor doesn't actually derive from IFormPage and thus
+ * doesn't have the get-by-page-id method. In this case, the method returns null since
+ * IEditorPart does not implement IFormPage.
+ */
+ @Override
+ public IFormPage setActivePage(String pageId) {
+ if (pageId.equals(TEXT_EDITOR_ID)) {
+ super.setActivePage(mTextPageIndex);
+ return null;
+ } else {
+ return super.setActivePage(pageId);
+ }
+ }
+
+
+ /**
+ * Notifies this multi-page editor that the page with the given id has been
+ * activated. This method is called when the user selects a different tab.
+ *
+ * @see MultiPageEditorPart#pageChange(int)
+ */
+ @Override
+ protected void pageChange(int newPageIndex) {
+ super.pageChange(newPageIndex);
+
+ // Do not record page changes during creation of pages
+ if (mIsCreatingPage) {
+ return;
+ }
+
+ if (getEditorInput() instanceof IFileEditorInput) {
+ IFile file = ((IFileEditorInput) getEditorInput()).getFile();
+
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ getClass().getSimpleName() + PREF_CURRENT_PAGE);
+ try {
+ file.setPersistentProperty(qname, Integer.toString(newPageIndex));
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Notifies this listener that some resource changes
+ * are happening, or have already happened.
+ *
+ * Closes all project files on project close.
+ * @see IResourceChangeListener
+ */
+ @Override
+ public void resourceChanged(final IResourceChangeEvent event) {
+ if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ @SuppressWarnings("hiding")
+ IWorkbenchPage[] pages = getSite().getWorkbenchWindow().getPages();
+ for (int i = 0; i < pages.length; i++) {
+ if (((FileEditorInput)mTextEditor.getEditorInput())
+ .getFile().getProject().equals(
+ event.getResource())) {
+ IEditorPart editorPart = pages[i].findEditor(mTextEditor
+ .getEditorInput());
+ pages[i].closeEditor(editorPart, true);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Initializes the editor part with a site and input.
+ * <p/>
+ * Checks that the input is an instance of {@link IFileEditorInput}.
+ *
+ * @see FormEditor
+ */
+ @Override
+ public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
+ if (!(editorInput instanceof IFileEditorInput))
+ throw new PartInitException("Invalid Input: Must be IFileEditorInput");
+ super.init(site, editorInput);
+ }
+
+ /**
+ * Returns the {@link IFile} matching the editor's input or null.
+ * <p/>
+ * By construction, the editor input has to be an {@link IFileEditorInput} so it must
+ * have an associated {@link IFile}. Null can only be returned if this editor has no
+ * input somehow.
+ */
+ public IFile getFile() {
+ if (getEditorInput() instanceof IFileEditorInput) {
+ return ((IFileEditorInput) getEditorInput()).getFile();
+ }
+ return null;
+ }
+
+ /**
+ * Removes attached listeners.
+ *
+ * @see WorkbenchPart
+ */
+ @Override
+ public void dispose() {
+ ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
+
+ super.dispose();
+ }
+
+ /**
+ * Commit all dirty pages then saves the contents of the text editor.
+ * <p/>
+ * This works by committing all data to the XML model and then
+ * asking the Structured XML Editor to save the XML.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ commitPages(true /* onSave */);
+
+ // The actual "save" operation is done by the Structured XML Editor
+ getEditor(mTextPageIndex).doSave(monitor);
+ }
+
+ /* (non-Javadoc)
+ * Saves the contents of this editor to another object.
+ * <p>
+ * Subclasses must override this method to implement the open-save-close lifecycle
+ * for an editor. For greater details, see <code>IEditorPart</code>
+ * </p>
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public void doSaveAs() {
+ commitPages(true /* onSave */);
+
+ IEditorPart editor = getEditor(mTextPageIndex);
+ editor.doSaveAs();
+ setPageText(mTextPageIndex, editor.getTitle());
+ setInput(editor.getEditorInput());
+ }
+
+ /**
+ * Commits all dirty pages in the editor. This method should
+ * be called as a first step of a 'save' operation.
+ * <p/>
+ * This is the same implementation as in {@link FormEditor}
+ * except it fixes two bugs: a cast to IFormPage is done
+ * from page.get(i) <em>before</em> being tested with instanceof.
+ * Another bug is that the last page might be a null pointer.
+ * <p/>
+ * The incorrect casting makes the original implementation crash due
+ * to our {@link StructuredTextEditor} not being an {@link IFormPage}
+ * so we have to override and duplicate to fix it.
+ *
+ * @param onSave <code>true</code> if commit is performed as part
+ * of the 'save' operation, <code>false</code> otherwise.
+ * @since 3.3
+ */
+ @Override
+ public void commitPages(boolean onSave) {
+ if (pages != null) {
+ for (int i = 0; i < pages.size(); i++) {
+ Object page = pages.get(i);
+ if (page != null && page instanceof IFormPage) {
+ IFormPage form_page = (IFormPage) page;
+ IManagedForm managed_form = form_page.getManagedForm();
+ if (managed_form != null && managed_form.isDirty()) {
+ managed_form.commit(onSave);
+ }
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p>
+ * Subclasses must override this method to implement the open-save-close lifecycle
+ * for an editor. For greater details, see <code>IEditorPart</code>
+ * </p>
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ // ---- Local methods ----
+
+
+ /**
+ * Helper method that creates a new hyper-link Listener.
+ * Used by derived classes which need active links in {@link FormText}.
+ * <p/>
+ * This link listener handles two kinds of URLs:
+ * <ul>
+ * <li> Links starting with "http" are simply sent to a local browser.
+ * <li> Links starting with "file:/" are simply sent to a local browser.
+ * <li> Links starting with "page:" are expected to be an editor page id to switch to.
+ * <li> Other links are ignored.
+ * </ul>
+ *
+ * @return A new hyper-link listener for FormText to use.
+ */
+ public final IHyperlinkListener createHyperlinkListener() {
+ return new HyperlinkAdapter() {
+ /**
+ * Switch to the page corresponding to the link that has just been clicked.
+ * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
+ */
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ String link = e.data.toString();
+ if (link.startsWith("http") || //$NON-NLS-1$
+ link.startsWith("file:/")) { //$NON-NLS-1$
+ openLinkInBrowser(link);
+ } else if (link.startsWith("page:")) { //$NON-NLS-1$
+ // Switch to an internal page
+ setActivePage(link.substring(5 /* strlen("page:") */));
+ }
+ }
+ };
+ }
+
+ /**
+ * Open the http link into a browser
+ *
+ * @param link The URL to open in a browser
+ */
+ private void openLinkInBrowser(String link) {
+ try {
+ IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
+ wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
+ } catch (PartInitException e1) {
+ // pass
+ } catch (MalformedURLException e1) {
+ // pass
+ }
+ }
+
+ /**
+ * Creates the XML source editor.
+ * <p/>
+ * Memorizes the index page of the source editor (it's always the last page, but the number
+ * of pages before can change.)
+ * <br/>
+ * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
+ * Finally triggers modelChanged() on the model listener -- derived classes can use this
+ * to initialize the model the first time.
+ * <p/>
+ * Called only once <em>after</em> createFormPages.
+ */
+ private void createTextEditor() {
+ try {
+ mTextEditor = new TextEditor();
+ int index = addPage(mTextEditor, getEditorInput());
+ mTextPageIndex = index;
+ setPageText(index, mTextEditor.getTitle());
+
+ IDocumentProvider provider = mTextEditor.getDocumentProvider();
+ mDocument = provider.getDocument(getEditorInput());
+
+ mDocument.addDocumentListener(new IDocumentListener() {
+ @Override
+ public void documentChanged(DocumentEvent event) {
+ onDocumentChanged(event);
+ }
+
+ @Override
+ public void documentAboutToBeChanged(DocumentEvent event) {
+ // ignore
+ }
+ });
+
+
+ } catch (PartInitException e) {
+ ErrorDialog.openError(getSite().getShell(),
+ "Android Text Editor Error", null, e.getStatus());
+ }
+ }
+
+ /**
+ * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
+ * the current file input.
+ * <p/>
+ * All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}.
+ * The actual document instance is a {@link SynchronizableDocument}, which creates a lock
+ * around read/set operations. The base API provided by {@link IDocument} provides ways to
+ * manipulate the document line per line or as a bulk.
+ */
+ public IDocument getDocument() {
+ return mDocument;
+ }
+
+ /**
+ * Returns the {@link IProject} for the edited file.
+ */
+ public IProject getProject() {
+ if (mTextEditor != null) {
+ IEditorInput input = mTextEditor.getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ IFile inputFile = fileInput.getFile();
+
+ if (inputFile != null) {
+ return inputFile.getProject();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Runs the given operation in the context of a document RewriteSession.
+ * Takes care of properly starting and stopping the operation.
+ * <p/>
+ * The operation itself should just access {@link #getDocument()} and use the
+ * normal document's API to manipulate it.
+ *
+ * @see #getDocument()
+ */
+ public void wrapRewriteSession(Runnable operation) {
+ if (mDocument instanceof IDocumentExtension4) {
+ IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
+
+ DocumentRewriteSession session = null;
+ try {
+ session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
+
+ operation.run();
+ } catch(IllegalStateException e) {
+ AdtPlugin.log(e, "wrapRewriteSession failed");
+ e.printStackTrace();
+ } finally {
+ if (session != null) {
+ doc4.stopRewriteSession(session);
+ }
+ }
+
+ } else {
+ // Not an IDocumentExtension4? Unlikely. Try the operation anyway.
+ operation.run();
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java
new file mode 100644
index 000000000..8a078efc2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_CONTENT;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_NAME;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentCommand;
+import org.eclipse.jface.text.IAutoEditStrategy;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ITextEditorExtension3;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+
+/**
+ * Edit strategy for Android XML files. It attempts a number of edit
+ * enhancements:
+ * <ul>
+ * <li> Auto indentation. The default XML indentation scheme is to just copy the
+ * indentation of the previous line. This edit strategy improves on that situation
+ * by considering the tag and bracket balance on the current line and using it
+ * to determine whether the next line should be indented or use the same
+ * indentation as the parent, or even the indentation of an earlier line
+ * (when for example the current line closes an element which was started on an
+ * earlier line.)
+ * <li> Newline handling. In addition to indenting, it can also adjust the following text
+ * appropriately when a newline is inserted. For example, it will reformat
+ * the following (where | represents the caret position):
+ * <pre>
+ * {@code <item name="a">|</item>}
+ * </pre>
+ * into
+ * <pre>
+ * {@code <item name="a">}
+ * |
+ * {@code </item>}
+ * </pre>
+ * </ul>
+ * In the future we might consider other editing enhancements here as well, such as
+ * refining the comment handling, or reindenting when you type the / of a closing tag,
+ * or even making the bracket matcher more resilient.
+ */
+@SuppressWarnings("restriction") // XML model
+public class AndroidXmlAutoEditStrategy implements IAutoEditStrategy {
+
+ @Override
+ public void customizeDocumentCommand(IDocument document, DocumentCommand c) {
+ if (!isSmartInsertMode()) {
+ return;
+ }
+
+ if (!(document instanceof IStructuredDocument)) {
+ // This shouldn't happen unless this strategy is used on an invalid document
+ return;
+ }
+ IStructuredDocument doc = (IStructuredDocument) document;
+
+ // Handle newlines/indentation
+ if (c.length == 0 && c.text != null
+ && TextUtilities.endsWith(doc.getLegalLineDelimiters(), c.text) != -1) {
+
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = modelManager.getModelForRead(doc);
+ if (model != null) {
+ try {
+ final int offset = c.offset;
+ int lineStart = findLineStart(doc, offset);
+ int textStart = findTextStart(doc, lineStart, offset);
+
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(textStart);
+ if (region != null && region.getType().equals(XML_TAG_NAME)) {
+ Pair<Integer,Integer> balance = getBalance(doc, textStart, offset);
+ int tagBalance = balance.getFirst();
+ int bracketBalance = balance.getSecond();
+
+ String lineIndent = ""; //$NON-NLS-1$
+ if (textStart > lineStart) {
+ lineIndent = doc.get(lineStart, textStart - lineStart);
+ }
+
+ // We only care if tag or bracket balance is greater than 0;
+ // we never *dedent* on negative balances
+ boolean addIndent = false;
+ if (bracketBalance < 0) {
+ // Handle
+ // <foo
+ // ></foo>^
+ // and
+ // <foo
+ // />^
+ ITextRegion left = getRegionAt(doc, offset, true /*biasLeft*/);
+ if (left != null
+ && (left.getType().equals(XML_TAG_CLOSE)
+ || left.getType().equals(XML_EMPTY_TAG_CLOSE))) {
+
+ // Find the corresponding open tag...
+ // The org.eclipse.wst.xml.ui.gotoMatchingTag frequently
+ // doesn't work, it just says "No matching brace found"
+ // (or I would use that here).
+
+ int targetBalance = 0;
+ ITextRegion right = getRegionAt(doc, offset, false /*biasLeft*/);
+ if (right != null && right.getType().equals(XML_END_TAG_OPEN)) {
+ targetBalance = -1;
+ }
+ int openTag = AndroidXmlCharacterMatcher.findTagBackwards(doc,
+ offset, targetBalance);
+ if (openTag != -1) {
+ // Look up the indentation of the given line
+ lineIndent = AndroidXmlEditor.getIndentAtOffset(doc, openTag);
+ }
+ }
+ } else if (tagBalance > 0 || bracketBalance > 0) {
+ // Add indentation
+ addIndent = true;
+ }
+
+ StringBuilder sb = new StringBuilder(c.text);
+ sb.append(lineIndent);
+ String oneIndentUnit = EclipseXmlFormatPreferences.create().getOneIndentUnit();
+ if (addIndent) {
+ sb.append(oneIndentUnit);
+ }
+
+ // Handle
+ // <foo>^</foo>
+ // turning into
+ // <foo>
+ // ^
+ // </foo>
+ ITextRegion left = getRegionAt(doc, offset, true /*biasLeft*/);
+ ITextRegion right = getRegionAt(doc, offset, false /*biasLeft*/);
+ if (left != null && right != null
+ && left.getType().equals(XML_TAG_CLOSE)
+ && right.getType().equals(XML_END_TAG_OPEN)) {
+ // Move end tag
+ if (tagBalance > 0 && bracketBalance < 0) {
+ sb.append(oneIndentUnit);
+ }
+ c.caretOffset = offset + sb.length();
+ c.shiftsCaret = false;
+ sb.append(TextUtilities.getDefaultLineDelimiter(doc));
+ sb.append(lineIndent);
+ }
+ c.text = sb.toString();
+ } else if (region != null && region.getType().equals(XML_CONTENT)) {
+ // Indenting in text content. If you're in the middle of editing
+ // text, just copy the current line indentation.
+ // However, if you're editing in leading whitespace (e.g. you press
+ // newline on a blank line following say an element) then figure
+ // out the indentation as if the newline had been pressed at the
+ // end of the element, and insert that amount of indentation.
+ // In this case we need to also make sure to subtract any existing
+ // whitespace on the current line such that if we have
+ //
+ // <foo>
+ // ^ <bar/>
+ // </foo>
+ //
+ // you end up with
+ //
+ // <foo>
+ //
+ // ^<bar/>
+ // </foo>
+ //
+ String text = region.getText();
+ int regionStart = region.getStartOffset();
+ int delta = offset - regionStart;
+ boolean inWhitespacePrefix = true;
+ for (int i = 0, n = Math.min(delta, text.length()); i < n; i++) {
+ char ch = text.charAt(i);
+ if (!Character.isWhitespace(ch)) {
+ inWhitespacePrefix = false;
+ break;
+ }
+ }
+ if (inWhitespacePrefix) {
+ IStructuredDocumentRegion previous = region.getPrevious();
+ if (previous != null && previous.getType() == XML_TAG_NAME) {
+ ITextRegionList subRegions = previous.getRegions();
+ ITextRegion last = subRegions.get(subRegions.size() - 1);
+ if (last.getType() == XML_TAG_CLOSE ||
+ last.getType() == XML_EMPTY_TAG_CLOSE) {
+ // See if the last tag was a closing tag
+ boolean wasClose = last.getType() == XML_EMPTY_TAG_CLOSE;
+ if (!wasClose) {
+ // Search backwards to see if the XML_TAG_CLOSE
+ // is the end of an </endtag>
+ for (int i = subRegions.size() - 2; i >= 0; i--) {
+ ITextRegion current = subRegions.get(i);
+ String type = current.getType();
+ if (type != XML_TAG_NAME) {
+ wasClose = type == XML_END_TAG_OPEN;
+ break;
+ }
+ }
+ }
+
+ int begin = AndroidXmlCharacterMatcher.findTagBackwards(doc,
+ previous.getStartOffset() + last.getStart(), 0);
+ int prevLineStart = findLineStart(doc, begin);
+ int prevTextStart = findTextStart(doc, prevLineStart, begin);
+
+ String lineIndent = ""; //$NON-NLS-1$
+ if (prevTextStart > prevLineStart) {
+ lineIndent = doc.get(prevLineStart,
+ prevTextStart - prevLineStart);
+ }
+ StringBuilder sb = new StringBuilder(c.text);
+ sb.append(lineIndent);
+
+ // See if there is whitespace on the insert line that
+ // we should also remove
+ for (int i = delta, n = text.length(); i < n; i++) {
+ char ch = text.charAt(i);
+ if (ch == ' ') {
+ c.length++;
+ } else {
+ break;
+ }
+ }
+
+ boolean addIndent = (last.getType() == XML_TAG_CLOSE)
+ && !wasClose;
+
+ // Is there just whitespace left of this text tag
+ // until we reach an end tag?
+ boolean whitespaceToEndTag = true;
+ for (int i = delta; i < text.length(); i++) {
+ char ch = text.charAt(i);
+ if (ch == '\n' || !Character.isWhitespace(ch)) {
+ whitespaceToEndTag = false;
+ break;
+ }
+ }
+ if (whitespaceToEndTag) {
+ IStructuredDocumentRegion next = region.getNext();
+ if (next != null && next.getType() == XML_TAG_NAME) {
+ String nextType = next.getRegions().get(0).getType();
+ if (nextType == XML_END_TAG_OPEN) {
+ addIndent = false;
+ }
+ }
+ }
+
+ if (addIndent) {
+ sb.append(EclipseXmlFormatPreferences.create()
+ .getOneIndentUnit());
+ }
+ c.text = sb.toString();
+
+ return;
+ }
+ }
+ }
+ copyPreviousLineIndentation(doc, c);
+ } else {
+ copyPreviousLineIndentation(doc, c);
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the offset of the start of the line (which might be whitespace)
+ *
+ * @param document the document
+ * @param offset an offset for a character anywhere on the line
+ * @return the offset of the first character on the line
+ * @throws BadLocationException if the offset is invalid
+ */
+ public static int findLineStart(IDocument document, int offset) throws BadLocationException {
+ offset = Math.max(0, Math.min(offset, document.getLength() - 1));
+ IRegion info = document.getLineInformationOfOffset(offset);
+ return info.getOffset();
+ }
+
+ /**
+ * Finds the first non-whitespace character on the given line
+ *
+ * @param document the document to search
+ * @param lineStart the offset of the beginning of the line
+ * @param lineEnd the offset of the end of the line, or the maximum position on the
+ * line to search
+ * @return the offset of the first non whitespace character, or the maximum position,
+ * whichever is smallest
+ * @throws BadLocationException if the offsets are invalid
+ */
+ public static int findTextStart(IDocument document, int lineStart, int lineEnd)
+ throws BadLocationException {
+ for (int offset = lineStart; offset < lineEnd; offset++) {
+ char c = document.getChar(offset);
+ if (c != ' ' && c != '\t') {
+ return offset;
+ }
+ }
+
+ return lineEnd;
+ }
+
+ /**
+ * Indent the new line the same way as the current line.
+ *
+ * @param doc the document to indent in
+ * @param command the document command to customize
+ * @throws BadLocationException if the offsets are invalid
+ */
+ private void copyPreviousLineIndentation(IDocument doc, DocumentCommand command)
+ throws BadLocationException {
+
+ if (command.offset == -1 || doc.getLength() == 0) {
+ return;
+ }
+
+ int lineStart = findLineStart(doc, command.offset);
+ int textStart = findTextStart(doc, lineStart, command.offset);
+
+ StringBuilder sb = new StringBuilder(command.text);
+ if (textStart > lineStart) {
+ sb.append(doc.get(lineStart, textStart - lineStart));
+ }
+
+ command.text = sb.toString();
+ }
+
+
+ /**
+ * Returns the subregion at the given offset, with a bias to the left or a bias to the
+ * right. In other words, if | represents the caret position, in the XML
+ * {@code <foo>|</bar>} then the subregion with bias left is the closing {@code >} and
+ * the subregion with bias right is the opening {@code </}.
+ *
+ * @param doc the document
+ * @param offset the offset in the document
+ * @param biasLeft whether we should look at the token on the left or on the right
+ * @return the subregion at the given offset, or null if not found
+ */
+ private static ITextRegion getRegionAt(IStructuredDocument doc, int offset,
+ boolean biasLeft) {
+ if (biasLeft) {
+ offset--;
+ }
+ IStructuredDocumentRegion region =
+ doc.getRegionAtCharacterOffset(offset);
+ if (region != null) {
+ return region.getRegionAtCharacterOffset(offset);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a pair of (tag-balance,bracket-balance) for the range textStart to offset.
+ *
+ * @param doc the document
+ * @param start the offset of the starting character (inclusive)
+ * @param end the offset of the ending character (exclusive)
+ * @return the balance of tags and brackets
+ */
+ private static Pair<Integer, Integer> getBalance(IStructuredDocument doc,
+ int start, int end) {
+ // Balance of open and closing tags
+ // <foo></foo> has tagBalance = 0, <foo> has tagBalance = 1
+ int tagBalance = 0;
+ // Balance of open and closing brackets
+ // <foo attr1="value1"> has bracketBalance = 1, <foo has bracketBalance = 1
+ int bracketBalance = 0;
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
+
+ if (region != null) {
+ boolean inOpenTag = true;
+ while (region != null && region.getStartOffset() < end) {
+ int regionStart = region.getStartOffset();
+ ITextRegionList subRegions = region.getRegions();
+ for (int i = 0, n = subRegions.size(); i < n; i++) {
+ ITextRegion subRegion = subRegions.get(i);
+ int subRegionStart = regionStart + subRegion.getStart();
+ int subRegionEnd = regionStart + subRegion.getEnd();
+ if (subRegionEnd < start || subRegionStart >= end) {
+ continue;
+ }
+ String type = subRegion.getType();
+
+ if (XML_TAG_OPEN.equals(type)) {
+ bracketBalance++;
+ inOpenTag = true;
+ } else if (XML_TAG_CLOSE.equals(type)) {
+ bracketBalance--;
+ if (inOpenTag) {
+ tagBalance++;
+ } else {
+ tagBalance--;
+ }
+ } else if (XML_END_TAG_OPEN.equals(type)) {
+ bracketBalance++;
+ inOpenTag = false;
+ } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
+ bracketBalance--;
+ }
+ }
+
+ region = region.getNext();
+ }
+ }
+
+ return Pair.of(tagBalance, bracketBalance);
+ }
+
+ /**
+ * Determine if we're in smart insert mode (if so, don't do any edit magic)
+ *
+ * @return true if the editor is in smart mode (or if it's an unknown editor type)
+ */
+ private static boolean isSmartInsertMode() {
+ ITextEditor textEditor = AdtUtils.getActiveTextEditor();
+ if (textEditor instanceof ITextEditorExtension3) {
+ ITextEditorExtension3 editor = (ITextEditorExtension3) textEditor;
+ return editor.getInsertMode() == ITextEditorExtension3.SMART_INSERT;
+ }
+
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlCharacterMatcher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlCharacterMatcher.java
new file mode 100644
index 000000000..8a12fe03b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlCharacterMatcher.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_NAME;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
+
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+import org.eclipse.wst.xml.ui.internal.text.XMLDocumentRegionEdgeMatcher;
+
+/**
+ * Custom version of the character matcher for XML files which adds the ability to
+ * jump between open and close tags in the XML file.
+ */
+@SuppressWarnings("restriction")
+public class AndroidXmlCharacterMatcher extends XMLDocumentRegionEdgeMatcher {
+ /**
+ * Constructs a new character matcher for Android XML files
+ */
+ public AndroidXmlCharacterMatcher() {
+ }
+
+ @Override
+ public IRegion match(IDocument doc, int offset) {
+ if (offset < 0 || offset >= doc.getLength()) {
+ return null;
+ }
+
+ IRegion match = findOppositeTag(doc, offset);
+ if (match != null) {
+ return match;
+ }
+
+ return super.match(doc, offset);
+ }
+
+ private IRegion findOppositeTag(IDocument document, int offset) {
+ if (!(document instanceof IStructuredDocument)) {
+ return null;
+ }
+ IStructuredDocument doc = (IStructuredDocument) document;
+
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ if (region == null) {
+ return null;
+ }
+
+ ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
+ if (subRegion == null) {
+ return null;
+ }
+ ITextRegionList subRegions = region.getRegions();
+ int index = subRegions.indexOf(subRegion);
+
+ String type = subRegion.getType();
+ boolean isOpenTag = false;
+ boolean isCloseTag = false;
+
+ if (type.equals(XML_TAG_OPEN)) {
+ isOpenTag = true;
+ } else if (type.equals(XML_END_TAG_OPEN)) {
+ isCloseTag = true;
+ } else if (!(type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) &&
+ (subRegion.getStart() + region.getStartOffset() == offset)) {
+ // Look to the left one character; we may have the case where you're
+ // pointing to the right of a tag, e.g.
+ // <foo>^text
+ offset--;
+ region = doc.getRegionAtCharacterOffset(offset);
+ if (region == null) {
+ return null;
+ }
+ subRegion = region.getRegionAtCharacterOffset(offset);
+ if (subRegion == null) {
+ return null;
+ }
+ type = subRegion.getType();
+
+ subRegions = region.getRegions();
+ index = subRegions.indexOf(subRegion);
+ }
+
+ if (type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) {
+ for (int i = index; i >= 0; i--) {
+ subRegion = subRegions.get(i);
+ type = subRegion.getType();
+ if (type.equals(XML_TAG_OPEN)) {
+ isOpenTag = true;
+ break;
+ } else if (type.equals(XML_END_TAG_OPEN)) {
+ isCloseTag = true;
+ break;
+ }
+ }
+ }
+
+ if (isOpenTag) {
+ // Find closing tag
+ int target = findTagForwards(doc, subRegion.getStart() + region.getStartOffset(), 0);
+ // Note - there is no point in looking up the whole region for the matching
+ // tag, because even if you pass a length greater than 1 here, the paint highlighter
+ // will only highlight a single character -- the *last* character of the region,
+ // not the whole region itself.
+ return new Region(target, 1);
+ } else if (isCloseTag) {
+ // Find open tag
+ int target = findTagBackwards(doc, subRegion.getStart() + region.getStartOffset(), -1);
+ return new Region(target, 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the corresponding open tag by searching backwards until the tag balance
+ * reaches a given target.
+ *
+ * @param doc the document
+ * @param offset the ending offset (where the search begins searching backwards from)
+ * @param targetTagBalance the balance to end the search at
+ * @return the offset of the beginning of the open tag
+ */
+ public static int findTagBackwards(IStructuredDocument doc, int offset, int targetTagBalance) {
+ // Balance of open and closing tags
+ int tagBalance = 0;
+ // Balance of open and closing brackets
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ if (region != null) {
+ boolean inEmptyTag = true;
+
+ while (region != null) {
+ int regionStart = region.getStartOffset();
+ ITextRegionList subRegions = region.getRegions();
+ for (int i = subRegions.size() - 1; i >= 0; i--) {
+ ITextRegion subRegion = subRegions.get(i);
+ int subRegionStart = regionStart + subRegion.getStart();
+ if (subRegionStart >= offset) {
+ continue;
+ }
+ String type = subRegion.getType();
+
+ // Iterate backwards and keep track of the tag balance such that
+ // we can find the corresponding opening tag
+
+ if (XML_TAG_OPEN.equals(type)) {
+ if (!inEmptyTag) {
+ tagBalance--;
+ }
+ if (tagBalance == targetTagBalance) {
+ return subRegionStart;
+ }
+ } else if (XML_END_TAG_OPEN.equals(type)) {
+ tagBalance++;
+ } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
+ inEmptyTag = true;
+ } else if (XML_TAG_CLOSE.equals(type)) {
+ inEmptyTag = false;
+ }
+ }
+
+ region = region.getPrevious();
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Finds the corresponding closing tag by searching forwards until the tag balance
+ * reaches a given target.
+ *
+ * @param doc the document
+ * @param start the starting offset (where the search begins searching forwards from)
+ * @param targetTagBalance the balance to end the search at
+ * @return the offset of the beginning of the closing tag
+ */
+ public static int findTagForwards(IStructuredDocument doc, int start, int targetTagBalance) {
+ int tagBalance = 0;
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
+
+ if (region != null) {
+ while (region != null) {
+ int regionStart = region.getStartOffset();
+ ITextRegionList subRegions = region.getRegions();
+ for (int i = 0, n = subRegions.size(); i < n; i++) {
+ ITextRegion subRegion = subRegions.get(i);
+ int subRegionStart = regionStart + subRegion.getStart();
+ int subRegionEnd = regionStart + subRegion.getEnd();
+ if (subRegionEnd < start) {
+ continue;
+ }
+ String type = subRegion.getType();
+
+ if (XML_TAG_OPEN.equals(type)) {
+ tagBalance++;
+ } else if (XML_END_TAG_OPEN.equals(type)) {
+ tagBalance--;
+ if (tagBalance == targetTagBalance) {
+ return subRegionStart;
+ }
+ } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
+ tagBalance--;
+ if (tagBalance == targetTagBalance) {
+ // We don't jump to matching tags within a self-closed tag
+ return -1;
+ }
+ }
+ }
+
+ region = region.getNext();
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
new file mode 100644
index 000000000..1d4e133b6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
@@ -0,0 +1,1709 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static org.eclipse.wst.sse.ui.internal.actions.StructuredTextEditorActionConstants.ACTION_NAME_FORMAT_DOCUMENT;
+
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IURIEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IHyperlinkListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.ide.IDEActionFactory;
+import org.eclipse.ui.ide.IGotoMarker;
+import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
+import org.eclipse.ui.part.MultiPageEditorPart;
+import org.eclipse.ui.part.WorkbenchPart;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+
+/**
+ * Multi-page form editor for Android XML files.
+ * <p/>
+ * It is designed to work with a {@link StructuredTextEditor} that will display an XML file.
+ * <br/>
+ * Derived classes must implement createFormPages to create the forms before the
+ * source editor. This can be a no-op if desired.
+ */
+@SuppressWarnings("restriction") // Uses XML model, which has no non-restricted replacement yet
+public abstract class AndroidXmlEditor extends FormEditor {
+
+ /** Icon used for the XML source page. */
+ public static final String ICON_XML_PAGE = "editor_page_source"; //$NON-NLS-1$
+
+ /** Preference name for the current page of this file */
+ private static final String PREF_CURRENT_PAGE = "_current_page"; //$NON-NLS-1$
+
+ /** Id string used to create the Android SDK browser */
+ private static String BROWSER_ID = "android"; //$NON-NLS-1$
+
+ /** Page id of the XML source editor, used for switching tabs programmatically */
+ public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
+
+ /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
+ public static final int TEXT_WIDTH_HINT = 50;
+
+ /** Page index of the text editor (always the last page) */
+ protected int mTextPageIndex;
+ /** The text editor */
+ private StructuredTextEditor mTextEditor;
+ /** Listener for the XML model from the StructuredEditor */
+ private XmlModelStateListener mXmlModelStateListener;
+ /** Listener to update the root node if the target of the file is changed because of a
+ * SDK location change or a project target change */
+ private TargetChangeListener mTargetListener = null;
+
+ /** flag set during page creation */
+ private boolean mIsCreatingPage = false;
+
+ /**
+ * Flag used to ignore XML model updates. For example, the flag is set during
+ * formatting. A format operation should completely preserve the semantics of the XML
+ * so the document listeners can use this flag to skip updating the model when edits
+ * are observed during a formatting operation
+ */
+ private boolean mIgnoreXmlUpdate;
+
+ /**
+ * Flag indicating we're inside {@link #wrapEditXmlModel(Runnable)}.
+ * This is a counter, which allows us to nest the edit XML calls.
+ * There is no pending operation when the counter is at zero.
+ */
+ private int mIsEditXmlModelPending;
+
+ /**
+ * Usually null, but during an editing operation, represents the highest
+ * node which should be formatted when the editing operation is complete.
+ */
+ private UiElementNode mFormatNode;
+
+ /**
+ * Whether {@link #mFormatNode} should be formatted recursively, or just
+ * the node itself (its arguments)
+ */
+ private boolean mFormatChildren;
+
+ /**
+ * Creates a form editor.
+ * <p/>
+ * Some derived classes will want to use {@link #addDefaultTargetListener()}
+ * to setup the default listener to monitor SDK target changes. This
+ * is no longer the default.
+ */
+ public AndroidXmlEditor() {
+ super();
+ }
+
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ super.init(site, input);
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded or ITargetChangeListener asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+
+ /**
+ * Setups a default {@link ITargetChangeListener} that will call
+ * {@link #initUiRootNode(boolean)} when the SDK or the target changes.
+ */
+ public void addDefaultTargetListener() {
+ if (mTargetListener == null) {
+ mTargetListener = new TargetChangeListener() {
+ @Override
+ public IProject getProject() {
+ return AndroidXmlEditor.this.getProject();
+ }
+
+ @Override
+ public void reload() {
+ commitPages(false /* onSave */);
+
+ // recreate the ui root node always
+ initUiRootNode(true /*force*/);
+ }
+ };
+ AdtPlugin.getDefault().addTargetListener(mTargetListener);
+ }
+ }
+
+ // ---- Abstract Methods ----
+
+ /**
+ * Returns the root node of the UI element hierarchy manipulated by the current
+ * UI node editor.
+ */
+ abstract public UiElementNode getUiRootNode();
+
+ /**
+ * Creates the various form pages.
+ * <p/>
+ * Derived classes must implement this to add their own specific tabs.
+ */
+ abstract protected void createFormPages();
+
+ /**
+ * Called by the base class {@link AndroidXmlEditor} once all pages (custom form pages
+ * as well as text editor page) have been created. This give a chance to deriving
+ * classes to adjust behavior once the text page has been created.
+ */
+ protected void postCreatePages() {
+ // Nothing in the base class.
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiManifestNode is recreated even if it already exists.
+ */
+ abstract protected void initUiRootNode(boolean force);
+
+ /**
+ * Subclasses should override this method to process the new XML Model, which XML
+ * root node is given.
+ *
+ * The base implementation is empty.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ abstract protected void xmlModelChanged(Document xml_doc);
+
+ /**
+ * Controls whether XML models are ignored or not.
+ *
+ * @param ignore when true, ignore all subsequent XML model updates, when false start
+ * processing XML model updates again
+ */
+ public void setIgnoreXmlUpdate(boolean ignore) {
+ mIgnoreXmlUpdate = ignore;
+ }
+
+ /**
+ * Returns whether XML model events are ignored or not. This is the case
+ * when we are deliberately modifying the document in a way which does not
+ * change the semantics (such as formatting), or when we have already
+ * directly updated the model ourselves.
+ *
+ * @return true if XML events should be ignored
+ */
+ public boolean getIgnoreXmlUpdate() {
+ return mIgnoreXmlUpdate;
+ }
+
+ // ---- Base Class Overrides, Interfaces Implemented ----
+
+ @Override
+ public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+ Object result = super.getAdapter(adapter);
+
+ if (result != null && adapter.equals(IGotoMarker.class) ) {
+ final IGotoMarker gotoMarker = (IGotoMarker) result;
+ return new IGotoMarker() {
+ @Override
+ public void gotoMarker(IMarker marker) {
+ gotoMarker.gotoMarker(marker);
+ try {
+ // Lint markers should always jump to XML text
+ if (marker.getType().equals(AdtConstants.MARKER_LINT)) {
+ IEditorPart editor = AdtUtils.getActiveEditor();
+ if (editor instanceof AndroidXmlEditor) {
+ AndroidXmlEditor xmlEditor = (AndroidXmlEditor) editor;
+ xmlEditor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ };
+ }
+
+ if (result == null && adapter == IContentOutlinePage.class) {
+ return getStructuredTextEditor().getAdapter(adapter);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates the pages of the multi-page editor.
+ */
+ @Override
+ protected void addPages() {
+ createAndroidPages();
+ selectDefaultPage(null /* defaultPageId */);
+ }
+
+ /**
+ * Creates the page for the Android Editors
+ */
+ public void createAndroidPages() {
+ mIsCreatingPage = true;
+ createFormPages();
+ createTextEditor();
+ updateActionBindings();
+ postCreatePages();
+ mIsCreatingPage = false;
+ }
+
+ /**
+ * Returns whether the editor is currently creating its pages.
+ */
+ public boolean isCreatingPages() {
+ return mIsCreatingPage;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p/>
+ * If the page is an instance of {@link IPageImageProvider}, the image returned by
+ * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab.
+ */
+ @Override
+ public int addPage(IFormPage page) throws PartInitException {
+ int index = super.addPage(page);
+ if (page instanceof IPageImageProvider) {
+ setPageImage(index, ((IPageImageProvider) page).getPageImage());
+ }
+ return index;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p/>
+ * If the editor is an instance of {@link IPageImageProvider}, the image returned by
+ * by {@link IPageImageProvider#getPageImage()} will be set on the page's tab.
+ */
+ @Override
+ public int addPage(IEditorPart editor, IEditorInput input) throws PartInitException {
+ int index = super.addPage(editor, input);
+ if (editor instanceof IPageImageProvider) {
+ setPageImage(index, ((IPageImageProvider) editor).getPageImage());
+ }
+ return index;
+ }
+
+ /**
+ * Creates undo redo (etc) actions for the editor site (so that it works for any page of this
+ * multi-page editor) by re-using the actions defined by the {@link StructuredTextEditor}
+ * (aka the XML text editor.)
+ */
+ protected void updateActionBindings() {
+ IActionBars bars = getEditorSite().getActionBars();
+ if (bars != null) {
+ IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
+ bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
+
+ action = mTextEditor.getAction(ActionFactory.REDO.getId());
+ bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
+
+ bars.setGlobalActionHandler(ActionFactory.DELETE.getId(),
+ mTextEditor.getAction(ActionFactory.DELETE.getId()));
+ bars.setGlobalActionHandler(ActionFactory.CUT.getId(),
+ mTextEditor.getAction(ActionFactory.CUT.getId()));
+ bars.setGlobalActionHandler(ActionFactory.COPY.getId(),
+ mTextEditor.getAction(ActionFactory.COPY.getId()));
+ bars.setGlobalActionHandler(ActionFactory.PASTE.getId(),
+ mTextEditor.getAction(ActionFactory.PASTE.getId()));
+ bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ mTextEditor.getAction(ActionFactory.SELECT_ALL.getId()));
+ bars.setGlobalActionHandler(ActionFactory.FIND.getId(),
+ mTextEditor.getAction(ActionFactory.FIND.getId()));
+ bars.setGlobalActionHandler(IDEActionFactory.BOOKMARK.getId(),
+ mTextEditor.getAction(IDEActionFactory.BOOKMARK.getId()));
+
+ bars.updateActionBars();
+ }
+ }
+
+ /**
+ * Clears the action bindings for the editor site.
+ */
+ protected void clearActionBindings(boolean includeUndoRedo) {
+ IActionBars bars = getEditorSite().getActionBars();
+ if (bars != null) {
+ // For some reason, undo/redo doesn't seem to work in the form editor.
+ // This appears to be the case for pure Eclipse form editors too, e.g. see
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=68423
+ // However, as a workaround we can use the *text* editor's underlying undo
+ // to revert operations being done in the UI, and the form automatically updates.
+ // Therefore, to work around this, we simply leave the text editor bindings
+ // in place if {@code includeUndoRedo} is not set
+ if (includeUndoRedo) {
+ bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), null);
+ bars.setGlobalActionHandler(ActionFactory.REDO.getId(), null);
+ }
+ bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), null);
+ bars.setGlobalActionHandler(ActionFactory.CUT.getId(), null);
+ bars.setGlobalActionHandler(ActionFactory.COPY.getId(), null);
+ bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), null);
+ bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), null);
+ bars.setGlobalActionHandler(ActionFactory.FIND.getId(), null);
+ bars.setGlobalActionHandler(IDEActionFactory.BOOKMARK.getId(), null);
+
+ bars.updateActionBars();
+ }
+ }
+
+ /**
+ * Selects the default active page.
+ * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
+ * find the default page in the properties of the {@link IResource} object being edited.
+ */
+ public void selectDefaultPage(String defaultPageId) {
+ if (defaultPageId == null) {
+ IFile file = getInputFile();
+ if (file != null) {
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ getClass().getSimpleName() + PREF_CURRENT_PAGE);
+ String pageId;
+ try {
+ pageId = file.getPersistentProperty(qname);
+ if (pageId != null) {
+ defaultPageId = pageId;
+ }
+ } catch (CoreException e) {
+ // ignored
+ }
+ }
+ }
+
+ if (defaultPageId != null) {
+ try {
+ setActivePage(Integer.parseInt(defaultPageId));
+ } catch (Exception e) {
+ // We can get NumberFormatException from parseInt but also
+ // AssertionError from setActivePage when the index is out of bounds.
+ // Generally speaking we just want to ignore any exception and fall back on the
+ // first page rather than crash the editor load. Logging the error is enough.
+ AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
+ }
+ } else if (AdtPrefs.getPrefs().isXmlEditorPreferred(getPersistenceCategory())) {
+ setActivePage(mTextPageIndex);
+ }
+ }
+
+ /** The layout editor */
+ public static final int CATEGORY_LAYOUT = 1 << 0;
+ /** The manifest editor */
+ public static final int CATEGORY_MANIFEST = 1 << 1;
+ /** Any other XML editor */
+ public static final int CATEGORY_OTHER = 1 << 2;
+
+ /**
+ * Returns the persistence category to use for this editor; this should be
+ * one of the {@code CATEGORY_} constants such as {@link #CATEGORY_MANIFEST},
+ * {@link #CATEGORY_LAYOUT}, {@link #CATEGORY_OTHER}, ...
+ * <p>
+ * The persistence category is used to group editors together when it comes
+ * to certain types of persistence metadata. For example, whether this type
+ * of file was most recently edited graphically or with an XML text editor.
+ * We'll open new files in the same text or graphical mode as the last time
+ * the user edited a file of the same persistence category.
+ * <p>
+ * Before we added the persistence category, we had a single boolean flag
+ * recording whether the XML files were most recently edited graphically or
+ * not. However, this meant that users can't for example prefer to edit
+ * Manifest files graphically and string files via XML. By splitting the
+ * editors up into categories, we can track the mode at a finer granularity,
+ * and still allow similar editors such as those used for animations and
+ * colors to be treated the same way.
+ *
+ * @return the persistence category constant
+ */
+ protected int getPersistenceCategory() {
+ return CATEGORY_OTHER;
+ }
+
+ /**
+ * Removes all the pages from the editor.
+ */
+ protected void removePages() {
+ int count = getPageCount();
+ for (int i = count - 1 ; i >= 0 ; i--) {
+ removePage(i);
+ }
+ }
+
+ /**
+ * Overrides the parent's setActivePage to be able to switch to the xml editor.
+ *
+ * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
+ * This is needed because the editor doesn't actually derive from IFormPage and thus
+ * doesn't have the get-by-page-id method. In this case, the method returns null since
+ * IEditorPart does not implement IFormPage.
+ */
+ @Override
+ public IFormPage setActivePage(String pageId) {
+ if (pageId.equals(TEXT_EDITOR_ID)) {
+ super.setActivePage(mTextPageIndex);
+ return null;
+ } else {
+ return super.setActivePage(pageId);
+ }
+ }
+
+ /**
+ * Notifies this multi-page editor that the page with the given id has been
+ * activated. This method is called when the user selects a different tab.
+ *
+ * @see MultiPageEditorPart#pageChange(int)
+ */
+ @Override
+ protected void pageChange(int newPageIndex) {
+ super.pageChange(newPageIndex);
+
+ // Do not record page changes during creation of pages
+ if (mIsCreatingPage) {
+ return;
+ }
+
+ IFile file = getInputFile();
+ if (file != null) {
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ getClass().getSimpleName() + PREF_CURRENT_PAGE);
+ try {
+ file.setPersistentProperty(qname, Integer.toString(newPageIndex));
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+
+ boolean isTextPage = newPageIndex == mTextPageIndex;
+ AdtPrefs.getPrefs().setXmlEditorPreferred(getPersistenceCategory(), isTextPage);
+ }
+
+ /**
+ * Returns true if the active page is the editor page
+ *
+ * @return true if the active page is the editor page
+ */
+ public boolean isEditorPageActive() {
+ return getActivePage() == mTextPageIndex;
+ }
+
+ /**
+ * Returns the {@link IFile} matching the editor's input or null.
+ */
+ @Nullable
+ public IFile getInputFile() {
+ IEditorInput input = getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ return ((IFileEditorInput) input).getFile();
+ }
+ return null;
+ }
+
+ /**
+ * Removes attached listeners.
+ *
+ * @see WorkbenchPart
+ */
+ @Override
+ public void dispose() {
+ IStructuredModel xml_model = getModelForRead();
+ if (xml_model != null) {
+ try {
+ if (mXmlModelStateListener != null) {
+ xml_model.removeModelStateListener(mXmlModelStateListener);
+ }
+
+ } finally {
+ xml_model.releaseFromRead();
+ }
+ }
+
+ if (mTargetListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetListener);
+ mTargetListener = null;
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Commit all dirty pages then saves the contents of the text editor.
+ * <p/>
+ * This works by committing all data to the XML model and then
+ * asking the Structured XML Editor to save the XML.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ commitPages(true /* onSave */);
+
+ if (AdtPrefs.getPrefs().isFormatOnSave()) {
+ IAction action = mTextEditor.getAction(ACTION_NAME_FORMAT_DOCUMENT);
+ if (action != null) {
+ try {
+ mIgnoreXmlUpdate = true;
+ action.run();
+ } finally {
+ mIgnoreXmlUpdate = false;
+ }
+ }
+ }
+
+ // The actual "save" operation is done by the Structured XML Editor
+ getEditor(mTextPageIndex).doSave(monitor);
+
+ // Check for errors on save, if enabled
+ if (AdtPrefs.getPrefs().isLintOnSave()) {
+ runLint();
+ }
+ }
+
+ /**
+ * Tells the editor to start a Lint check.
+ * It's up to the caller to check whether this should be done depending on preferences.
+ * <p/>
+ * The default implementation is to call {@link #startLintJob()}.
+ *
+ * @return The Job started by {@link EclipseLintRunner} or null if no job was started.
+ */
+ protected Job runLint() {
+ return startLintJob();
+ }
+
+ /**
+ * Utility method that creates a Job to run Lint on the current document.
+ * Does not wait for the job to finish - just returns immediately.
+ *
+ * @return a new job, or null
+ * @see EclipseLintRunner#startLint(java.util.List, IResource, IDocument,
+ * boolean, boolean)
+ */
+ @Nullable
+ public Job startLintJob() {
+ IFile file = getInputFile();
+ if (file != null) {
+ return EclipseLintRunner.startLint(Collections.singletonList(file), file,
+ getStructuredDocument(), false /*fatalOnly*/, false /*show*/);
+ }
+
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * Saves the contents of this editor to another object.
+ * <p>
+ * Subclasses must override this method to implement the open-save-close lifecycle
+ * for an editor. For greater details, see <code>IEditorPart</code>
+ * </p>
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public void doSaveAs() {
+ commitPages(true /* onSave */);
+
+ IEditorPart editor = getEditor(mTextPageIndex);
+ editor.doSaveAs();
+ setPageText(mTextPageIndex, editor.getTitle());
+ setInput(editor.getEditorInput());
+ }
+
+ /**
+ * Commits all dirty pages in the editor. This method should
+ * be called as a first step of a 'save' operation.
+ * <p/>
+ * This is the same implementation as in {@link FormEditor}
+ * except it fixes two bugs: a cast to IFormPage is done
+ * from page.get(i) <em>before</em> being tested with instanceof.
+ * Another bug is that the last page might be a null pointer.
+ * <p/>
+ * The incorrect casting makes the original implementation crash due
+ * to our {@link StructuredTextEditor} not being an {@link IFormPage}
+ * so we have to override and duplicate to fix it.
+ *
+ * @param onSave <code>true</code> if commit is performed as part
+ * of the 'save' operation, <code>false</code> otherwise.
+ * @since 3.3
+ */
+ @Override
+ public void commitPages(boolean onSave) {
+ if (pages != null) {
+ for (int i = 0; i < pages.size(); i++) {
+ Object page = pages.get(i);
+ if (page != null && page instanceof IFormPage) {
+ IFormPage form_page = (IFormPage) page;
+ IManagedForm managed_form = form_page.getManagedForm();
+ if (managed_form != null && managed_form.isDirty()) {
+ managed_form.commit(onSave);
+ }
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p>
+ * Subclasses must override this method to implement the open-save-close lifecycle
+ * for an editor. For greater details, see <code>IEditorPart</code>
+ * </p>
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ /**
+ * Returns the page index of the text editor (always the last page)
+
+ * @return the page index of the text editor (always the last page)
+ */
+ public int getTextPageIndex() {
+ return mTextPageIndex;
+ }
+
+ // ---- Local methods ----
+
+
+ /**
+ * Helper method that creates a new hyper-link Listener.
+ * Used by derived classes which need active links in {@link FormText}.
+ * <p/>
+ * This link listener handles two kinds of URLs:
+ * <ul>
+ * <li> Links starting with "http" are simply sent to a local browser.
+ * <li> Links starting with "file:/" are simply sent to a local browser.
+ * <li> Links starting with "page:" are expected to be an editor page id to switch to.
+ * <li> Other links are ignored.
+ * </ul>
+ *
+ * @return A new hyper-link listener for FormText to use.
+ */
+ public final IHyperlinkListener createHyperlinkListener() {
+ return new HyperlinkAdapter() {
+ /**
+ * Switch to the page corresponding to the link that has just been clicked.
+ * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
+ */
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ String link = e.data.toString();
+ if (link.startsWith("http") || //$NON-NLS-1$
+ link.startsWith("file:/")) { //$NON-NLS-1$
+ openLinkInBrowser(link);
+ } else if (link.startsWith("page:")) { //$NON-NLS-1$
+ // Switch to an internal page
+ setActivePage(link.substring(5 /* strlen("page:") */));
+ }
+ }
+ };
+ }
+
+ /**
+ * Open the http link into a browser
+ *
+ * @param link The URL to open in a browser
+ */
+ private void openLinkInBrowser(String link) {
+ try {
+ IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
+ wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
+ } catch (PartInitException e1) {
+ // pass
+ } catch (MalformedURLException e1) {
+ // pass
+ }
+ }
+
+ /**
+ * Creates the XML source editor.
+ * <p/>
+ * Memorizes the index page of the source editor (it's always the last page, but the number
+ * of pages before can change.)
+ * <br/>
+ * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
+ * Finally triggers modelChanged() on the model listener -- derived classes can use this
+ * to initialize the model the first time.
+ * <p/>
+ * Called only once <em>after</em> createFormPages.
+ */
+ private void createTextEditor() {
+ try {
+ mTextEditor = new StructuredTextEditor() {
+ @Override
+ protected void createActions() {
+ super.createActions();
+
+ Action action = new RenameResourceXmlTextAction(mTextEditor);
+ action.setActionDefinitionId(IJavaEditorActionDefinitionIds.RENAME_ELEMENT);
+ setAction(IJavaEditorActionDefinitionIds.RENAME_ELEMENT, action);
+ }
+ };
+ int index = addPage(mTextEditor, getEditorInput());
+ mTextPageIndex = index;
+ setPageText(index, mTextEditor.getTitle());
+ setPageImage(index,
+ IconFactory.getInstance().getIcon(ICON_XML_PAGE));
+
+ if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) {
+ Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Error opening the Android XML editor. Is the document an XML file?");
+ throw new RuntimeException("Android XML Editor Error", new CoreException(status));
+ }
+
+ IStructuredModel xml_model = getModelForRead();
+ if (xml_model != null) {
+ try {
+ mXmlModelStateListener = new XmlModelStateListener();
+ xml_model.addModelStateListener(mXmlModelStateListener);
+ mXmlModelStateListener.modelChanged(xml_model);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$
+ } finally {
+ xml_model.releaseFromRead();
+ }
+ }
+ } catch (PartInitException e) {
+ ErrorDialog.openError(getSite().getShell(),
+ "Android XML Editor Error", null, e.getStatus());
+ }
+ }
+
+ /**
+ * Returns the ISourceViewer associated with the Structured Text editor.
+ */
+ public final ISourceViewer getStructuredSourceViewer() {
+ if (mTextEditor != null) {
+ // We can't access mDelegate.getSourceViewer() because it is protected,
+ // however getTextViewer simply returns the SourceViewer casted, so we
+ // can use it instead.
+ return mTextEditor.getTextViewer();
+ }
+ return null;
+ }
+
+ /**
+ * Return the {@link StructuredTextEditor} associated with this XML editor
+ *
+ * @return the associated {@link StructuredTextEditor}
+ */
+ public StructuredTextEditor getStructuredTextEditor() {
+ return mTextEditor;
+ }
+
+ /**
+ * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source
+ * Editor) or null if not available.
+ */
+ public IStructuredDocument getStructuredDocument() {
+ if (mTextEditor != null && mTextEditor.getTextViewer() != null) {
+ return (IStructuredDocument) mTextEditor.getTextViewer().getDocument();
+ }
+ return null;
+ }
+
+ /**
+ * Returns a version of the model that has been shared for read.
+ * <p/>
+ * Callers <em>must</em> call model.releaseFromRead() when done, typically
+ * in a try..finally clause.
+ *
+ * Portability note: this uses getModelManager which is part of wst.sse.core; however
+ * the interface returned is part of wst.sse.core.internal.provisional so we can
+ * expect it to change in a distant future if they start cleaning their codebase,
+ * however unlikely that is.
+ *
+ * @return The model for the XML document or null if cannot be obtained from the editor
+ */
+ public IStructuredModel getModelForRead() {
+ IStructuredDocument document = getStructuredDocument();
+ if (document != null) {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ // TODO simplify this by not using the internal IStructuredDocument.
+ // Instead we can now use mm.getModelForRead(getFile()).
+ // However we must first check that SSE for Eclipse 3.3 or 3.4 has this
+ // method. IIRC 3.3 didn't have it.
+
+ return mm.getModelForRead(document);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a version of the model that has been shared for edit.
+ * <p/>
+ * Callers <em>must</em> call model.releaseFromEdit() when done, typically
+ * in a try..finally clause.
+ * <p/>
+ * Because of this, it is mandatory to use the wrapper
+ * {@link #wrapEditXmlModel(Runnable)} which executes a runnable into a
+ * properly configured model and then performs whatever cleanup is necessary.
+ *
+ * @return The model for the XML document or null if cannot be obtained from the editor
+ */
+ private IStructuredModel getModelForEdit() {
+ IStructuredDocument document = getStructuredDocument();
+ if (document != null) {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ // TODO simplify this by not using the internal IStructuredDocument.
+ // Instead we can now use mm.getModelForRead(getFile()).
+ // However we must first check that SSE for Eclipse 3.3 or 3.4 has this
+ // method. IIRC 3.3 didn't have it.
+
+ return mm.getModelForEdit(document);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper class to perform edits on the XML model whilst making sure the
+ * model has been prepared to be changed.
+ * <p/>
+ * It first gets a model for edition using {@link #getModelForEdit()},
+ * then calls {@link IStructuredModel#aboutToChangeModel()},
+ * then performs the requested action
+ * and finally calls {@link IStructuredModel#changedModel()}
+ * and {@link IStructuredModel#releaseFromEdit()}.
+ * <p/>
+ * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method
+ * is called, XML model listeners will be triggered.
+ * <p/>
+ * Calls can be nested: only the first outer call will actually start and close the edit
+ * session.
+ * <p/>
+ * This method is <em>not synchronized</em> and is not thread safe.
+ * Callers must be using it from the the main UI thread.
+ *
+ * @param editAction Something that will change the XML.
+ */
+ public final void wrapEditXmlModel(Runnable editAction) {
+ wrapEditXmlModel(editAction, null);
+ }
+
+ /**
+ * Perform any editor-specific hooks after applying an edit. When edits are
+ * nested, the hooks will only run after the final top level edit has been
+ * performed.
+ * <p>
+ * Note that the edit hooks are performed outside of the edit lock so
+ * the hooks should not perform edits on the model without acquiring
+ * a lock first.
+ */
+ public void runEditHooks() {
+ if (!mIgnoreXmlUpdate) {
+ // Check for errors, if enabled
+ if (AdtPrefs.getPrefs().isLintOnSave()) {
+ runLint();
+ }
+ }
+ }
+
+ /**
+ * Executor which performs the given action under an edit lock (and optionally as a
+ * single undo event).
+ *
+ * @param editAction the action to be executed
+ * @param undoLabel if non null, the edit action will be run as a single undo event
+ * and the label used as the name of the undoable action
+ */
+ private final void wrapEditXmlModel(final Runnable editAction, final String undoLabel) {
+ Display display = mTextEditor.getSite().getShell().getDisplay();
+ if (display.getThread() != Thread.currentThread()) {
+ display.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!mTextEditor.getTextViewer().getControl().isDisposed()) {
+ wrapEditXmlModel(editAction, undoLabel);
+ }
+ }
+ });
+ return;
+ }
+
+ IStructuredModel model = null;
+ int undoReverseCount = 0;
+ try {
+
+ if (mIsEditXmlModelPending == 0) {
+ try {
+ model = getModelForEdit();
+ if (undoLabel != null) {
+ // Run this action as an undoable unit.
+ // We have to do it more than once, because in some scenarios
+ // Eclipse WTP decides to cancel the current undo command on its
+ // own -- see http://code.google.com/p/android/issues/detail?id=15901
+ // for one such call chain. By nesting these calls several times
+ // we've incrementing the command count such that a couple of
+ // cancellations are ignored. Interfering with this mechanism may
+ // sound dangerous, but it appears that this undo-termination is
+ // done for UI reasons to anticipate what the user wants, and we know
+ // that in *our* scenarios we want the entire unit run as a single
+ // unit. Here's what the documentation for
+ // IStructuredTextUndoManager#forceEndOfPendingCommand says
+ // "Normally, the undo manager can figure out the best
+ // times when to end a pending command and begin a new
+ // one ... to the structure of a structured
+ // document. There are times, however, when clients may
+ // wish to override those algorithms and end one earlier
+ // than normal. The one known case is for multi-page
+ // editors. If a user is on one page, and type '123' as
+ // attribute value, then click around to other parts of
+ // page, or different pages, then return to '123|' and
+ // type 456, then "undo" they typically expect the undo
+ // to just undo what they just typed, the 456, not the
+ // whole attribute value."
+ for (int i = 0; i < 4; i++) {
+ model.beginRecording(this, undoLabel);
+ undoReverseCount++;
+ }
+ }
+ model.aboutToChangeModel();
+ } catch (Throwable t) {
+ // This is never supposed to happen unless we suddenly don't have a model.
+ // If it does, we don't want to even try to modify anyway.
+ AdtPlugin.log(t, "XML Editor failed to get model to edit"); //$NON-NLS-1$
+ return;
+ }
+ }
+ mIsEditXmlModelPending++;
+ editAction.run();
+ } finally {
+ mIsEditXmlModelPending--;
+ if (model != null) {
+ try {
+ boolean oldIgnore = mIgnoreXmlUpdate;
+ try {
+ mIgnoreXmlUpdate = true;
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) {
+ if (mFormatNode == getUiRootNode()) {
+ reformatDocument();
+ } else {
+ Node node = mFormatNode.getXmlNode();
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int begin = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (!mFormatChildren) {
+ // This will format just the attribute list
+ end = begin + 1;
+ }
+
+ if (mFormatChildren
+ && node == node.getOwnerDocument().getDocumentElement()) {
+ reformatDocument();
+ } else {
+ reformatRegion(begin, end);
+ }
+ }
+ }
+ mFormatNode = null;
+ mFormatChildren = false;
+ }
+
+ // Notify the model we're done modifying it. This must *always* be executed.
+ model.changedModel();
+
+ // Clean up the undo unit. This is done more than once as explained
+ // above for beginRecording.
+ for (int i = 0; i < undoReverseCount; i++) {
+ model.endRecording(this);
+ }
+ } finally {
+ mIgnoreXmlUpdate = oldIgnore;
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Failed to clean up undo unit");
+ }
+ model.releaseFromEdit();
+
+ if (mIsEditXmlModelPending < 0) {
+ AdtPlugin.log(IStatus.ERROR,
+ "wrapEditXmlModel finished with invalid nested counter==%1$d", //$NON-NLS-1$
+ mIsEditXmlModelPending);
+ mIsEditXmlModelPending = 0;
+ }
+
+ runEditHooks();
+
+ // Notify listeners
+ IStructuredModel readModel = getModelForRead();
+ if (readModel != null) {
+ try {
+ mXmlModelStateListener.modelChanged(readModel);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Error while notifying changes"); //$NON-NLS-1$
+ } finally {
+ readModel.releaseFromRead();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Does this editor participate in the "format GUI editor changes" option?
+ *
+ * @return true if this editor supports automatically formatting XML
+ * affected by GUI changes
+ */
+ public boolean supportsFormatOnGuiEdit() {
+ return false;
+ }
+
+ /**
+ * Mark the given node as needing to be formatted when the current edits are
+ * done, provided the user has turned that option on (see
+ * {@link AdtPrefs#getFormatGuiXml()}).
+ *
+ * @param node the node to be scheduled for formatting
+ * @param attributesOnly if true, only update the attributes list of the
+ * node, otherwise update the node recursively (e.g. all children
+ * too)
+ */
+ public void scheduleNodeReformat(UiElementNode node, boolean attributesOnly) {
+ if (!supportsFormatOnGuiEdit()) {
+ return;
+ }
+
+ if (node == mFormatNode) {
+ if (!attributesOnly) {
+ mFormatChildren = true;
+ }
+ } else if (mFormatNode == null) {
+ mFormatNode = node;
+ mFormatChildren = !attributesOnly;
+ } else {
+ if (mFormatNode.isAncestorOf(node)) {
+ mFormatChildren = true;
+ } else if (node.isAncestorOf(mFormatNode)) {
+ mFormatNode = node;
+ mFormatChildren = true;
+ } else {
+ // Two independent nodes; format their closest common ancestor.
+ // Later we could consider having a small number of independent nodes
+ // and formatting those, and only switching to formatting the common ancestor
+ // when the number of individual nodes gets large.
+ mFormatChildren = true;
+ mFormatNode = UiElementNode.getCommonAncestor(mFormatNode, node);
+ }
+ }
+ }
+
+ /**
+ * Creates an "undo recording" session by calling the undoableAction runnable
+ * under an undo session.
+ * <p/>
+ * This also automatically starts an edit XML session, as if
+ * {@link #wrapEditXmlModel(Runnable)} had been called.
+ * <p>
+ * You can nest several calls to {@link #wrapUndoEditXmlModel(String, Runnable)}, only one
+ * recording session will be created.
+ *
+ * @param label The label for the undo operation. Can be null. Ideally we should really try
+ * to put something meaningful if possible.
+ * @param undoableAction the action to be run as a single undoable unit
+ */
+ public void wrapUndoEditXmlModel(String label, Runnable undoableAction) {
+ assert label != null : "All undoable actions should have a label";
+ wrapEditXmlModel(undoableAction, label == null ? "" : label); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns true when the runnable of {@link #wrapEditXmlModel(Runnable)} is currently
+ * being executed. This means it is safe to actually edit the XML model.
+ *
+ * @return true if the XML model is already locked for edits
+ */
+ public boolean isEditXmlModelPending() {
+ return mIsEditXmlModelPending > 0;
+ }
+
+ /**
+ * Returns the XML {@link Document} or null if we can't get it
+ */
+ public final Document getXmlDocument(IStructuredModel model) {
+ if (model == null) {
+ AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$
+ return null;
+ }
+
+ if (model instanceof IDOMModel) {
+ IDOMModel dom_model = (IDOMModel) model;
+ return dom_model.getDocument();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link IProject} for the edited file.
+ */
+ @Nullable
+ public IProject getProject() {
+ IFile file = getInputFile();
+ if (file != null) {
+ return file.getProject();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link AndroidTargetData} for the edited file.
+ */
+ @Nullable
+ public AndroidTargetData getTargetData() {
+ IProject project = getProject();
+ if (project != null) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+
+ if (target != null) {
+ return currentSdk.getTargetData(target);
+ }
+ }
+ }
+
+ IEditorInput input = getEditorInput();
+ if (input instanceof IURIEditorInput) {
+ IURIEditorInput urlInput = (IURIEditorInput) input;
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ try {
+ String path = AdtUtils.getFile(urlInput.getURI().toURL()).getPath();
+ IAndroidTarget[] targets = currentSdk.getTargets();
+ for (IAndroidTarget target : targets) {
+ if (path.startsWith(target.getLocation())) {
+ return currentSdk.getTargetData(target);
+ }
+ }
+ } catch (MalformedURLException e) {
+ // File might be in some other weird random location we can't
+ // handle: Just ignore these
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Shows the editor range corresponding to the given XML node. This will
+ * front the editor and select the text range.
+ *
+ * @param xmlNode The DOM node to be shown. The DOM node should be an XML
+ * node from the existing XML model used by the structured XML
+ * editor; it will not do attribute matching to find a
+ * "corresponding" element in the document from some foreign DOM
+ * tree.
+ * @return True if the node was shown.
+ */
+ public boolean show(Node xmlNode) {
+ if (xmlNode instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion)xmlNode;
+
+ IEditorPart textPage = getEditor(mTextPageIndex);
+ if (textPage instanceof StructuredTextEditor) {
+ StructuredTextEditor editor = (StructuredTextEditor) textPage;
+
+ setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
+
+ // Note - we cannot use region.getLength() because that seems to
+ // always return 0.
+ int regionLength = region.getEndOffset() - region.getStartOffset();
+ editor.selectAndReveal(region.getStartOffset(), regionLength);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Selects and reveals the given range in the text editor
+ *
+ * @param start the beginning offset
+ * @param length the length of the region to show
+ * @param frontTab if true, front the tab, otherwise just make the selection but don't
+ * change the active tab
+ */
+ public void show(int start, int length, boolean frontTab) {
+ IEditorPart textPage = getEditor(mTextPageIndex);
+ if (textPage instanceof StructuredTextEditor) {
+ StructuredTextEditor editor = (StructuredTextEditor) textPage;
+ if (frontTab) {
+ setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
+ }
+ editor.selectAndReveal(start, length);
+ if (frontTab) {
+ editor.setFocus();
+ }
+ }
+ }
+
+ /**
+ * Returns true if this editor has more than one page (usually a graphical view and an
+ * editor)
+ *
+ * @return true if this editor has multiple pages
+ */
+ public boolean hasMultiplePages() {
+ return getPageCount() > 1;
+ }
+
+ /**
+ * Get the XML text directly from the editor.
+ *
+ * @param xmlNode The node whose XML text we want to obtain.
+ * @return The XML representation of the {@link Node}, or null if there was an error.
+ */
+ public String getXmlText(Node xmlNode) {
+ String data = null;
+ IStructuredModel model = getModelForRead();
+ try {
+ IStructuredDocument document = getStructuredDocument();
+ if (xmlNode instanceof NodeContainer) {
+ // The easy way to get the source of an SSE XML node.
+ data = ((NodeContainer) xmlNode).getSource();
+ } else if (xmlNode instanceof IndexedRegion && document != null) {
+ // Try harder.
+ IndexedRegion region = (IndexedRegion) xmlNode;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (end > start) {
+ data = document.get(start, end - start);
+ }
+ }
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ } finally {
+ model.releaseFromRead();
+ }
+ return data;
+ }
+
+ /**
+ * Formats the text around the given caret range, using the current Eclipse
+ * XML formatter settings.
+ *
+ * @param begin The starting offset of the range to be reformatted.
+ * @param end The ending offset of the range to be reformatted.
+ */
+ public void reformatRegion(int begin, int end) {
+ ISourceViewer textViewer = getStructuredSourceViewer();
+
+ // Clamp text range to valid offsets.
+ IDocument document = textViewer.getDocument();
+ int documentLength = document.getLength();
+ end = Math.min(end, documentLength);
+ begin = Math.min(begin, end);
+
+ if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) {
+ // Workarounds which only apply to the builtin Eclipse formatter:
+ //
+ // It turns out the XML formatter does *NOT* format things correctly if you
+ // select just a region of text. You *MUST* also include the leading whitespace
+ // on the line, or it will dedent all the content to column 0. Therefore,
+ // we must figure out the offset of the start of the line that contains the
+ // beginning of the tag.
+ try {
+ IRegion lineInformation = document.getLineInformationOfOffset(begin);
+ if (lineInformation != null) {
+ int lineBegin = lineInformation.getOffset();
+ if (lineBegin != begin) {
+ begin = lineBegin;
+ } else if (begin > 0) {
+ // Trick #2: It turns out that, if an XML element starts in column 0,
+ // then the XML formatter will NOT indent it (even if its parent is
+ // indented). If you on the other hand include the end of the previous
+ // line (the newline), THEN the formatter also correctly inserts the
+ // element. Therefore, we adjust the beginning range to include the
+ // previous line (if we are not already in column 0 of the first line)
+ // in the case where the element starts the line.
+ begin--;
+ }
+ }
+ } catch (BadLocationException e) {
+ // This cannot happen because we already clamped the offsets
+ AdtPlugin.log(e, e.toString());
+ }
+ }
+
+ if (textViewer instanceof StructuredTextViewer) {
+ StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+ int operation = ISourceViewer.FORMAT;
+ boolean canFormat = structuredTextViewer.canDoOperation(operation);
+ if (canFormat) {
+ StyledText textWidget = textViewer.getTextWidget();
+ textWidget.setSelection(begin, end);
+
+ boolean oldIgnore = mIgnoreXmlUpdate;
+ try {
+ // Formatting does not affect the XML model so ignore notifications
+ // about model edits from this
+ mIgnoreXmlUpdate = true;
+ structuredTextViewer.doOperation(operation);
+ } finally {
+ mIgnoreXmlUpdate = oldIgnore;
+ }
+
+ textWidget.setSelection(0, 0);
+ }
+ }
+ }
+
+ /**
+ * Invokes content assist in this editor at the given offset
+ *
+ * @param offset the offset to invoke content assist at, or -1 to leave
+ * caret alone
+ */
+ public void invokeContentAssist(int offset) {
+ ISourceViewer textViewer = getStructuredSourceViewer();
+ if (textViewer instanceof StructuredTextViewer) {
+ StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+ int operation = ISourceViewer.CONTENTASSIST_PROPOSALS;
+ boolean allowed = structuredTextViewer.canDoOperation(operation);
+ if (allowed) {
+ if (offset != -1) {
+ StyledText textWidget = textViewer.getTextWidget();
+ // Clamp text range to valid offsets.
+ IDocument document = textViewer.getDocument();
+ int documentLength = document.getLength();
+ offset = Math.max(0, Math.min(offset, documentLength));
+ textWidget.setSelection(offset, offset);
+ }
+ structuredTextViewer.doOperation(operation);
+ }
+ }
+ }
+
+ /**
+ * Formats the XML region corresponding to the given node.
+ *
+ * @param node The node to be formatted.
+ */
+ public void reformatNode(Node node) {
+ if (mIsCreatingPage) {
+ return;
+ }
+
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int begin = region.getStartOffset();
+ int end = region.getEndOffset();
+ reformatRegion(begin, end);
+ }
+ }
+
+ /**
+ * Formats the XML document according to the user's XML formatting settings.
+ */
+ public void reformatDocument() {
+ ISourceViewer textViewer = getStructuredSourceViewer();
+ if (textViewer instanceof StructuredTextViewer) {
+ StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+ int operation = StructuredTextViewer.FORMAT_DOCUMENT;
+ boolean canFormat = structuredTextViewer.canDoOperation(operation);
+ if (canFormat) {
+ boolean oldIgnore = mIgnoreXmlUpdate;
+ try {
+ // Formatting does not affect the XML model so ignore notifications
+ // about model edits from this
+ mIgnoreXmlUpdate = true;
+ structuredTextViewer.doOperation(operation);
+ } finally {
+ mIgnoreXmlUpdate = oldIgnore;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the indentation String of the given node.
+ *
+ * @param xmlNode The node whose indentation we want.
+ * @return The indent-string of the given node, or "" if the indentation for some reason could
+ * not be computed.
+ */
+ public String getIndent(Node xmlNode) {
+ return getIndent(getStructuredDocument(), xmlNode);
+ }
+
+ /**
+ * Returns the indentation String of the given node.
+ *
+ * @param document The Eclipse document containing the XML
+ * @param xmlNode The node whose indentation we want.
+ * @return The indent-string of the given node, or "" if the indentation for some reason could
+ * not be computed.
+ */
+ public static String getIndent(IDocument document, Node xmlNode) {
+ if (xmlNode instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion)xmlNode;
+ int startOffset = region.getStartOffset();
+ return getIndentAtOffset(document, startOffset);
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the indentation String at the line containing the given offset
+ *
+ * @param document the document containing the offset
+ * @param offset The offset of a character on a line whose indentation we seek
+ * @return The indent-string of the given node, or "" if the indentation for some
+ * reason could not be computed.
+ */
+ public static String getIndentAtOffset(IDocument document, int offset) {
+ try {
+ IRegion lineInformation = document.getLineInformationOfOffset(offset);
+ if (lineInformation != null) {
+ int lineBegin = lineInformation.getOffset();
+ if (lineBegin != offset) {
+ String prefix = document.get(lineBegin, offset - lineBegin);
+
+ // It's possible that the tag whose indentation we seek is not
+ // at the beginning of the line. In that case we'll just return
+ // the indentation of the line itself.
+ for (int i = 0; i < prefix.length(); i++) {
+ if (!Character.isWhitespace(prefix.charAt(i))) {
+ return prefix.substring(0, i);
+ }
+ }
+
+ return prefix;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, "Could not obtain indentation"); //$NON-NLS-1$
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the active {@link AndroidXmlEditor}, provided it matches the given source
+ * viewer
+ *
+ * @param viewer the source viewer to ensure the active editor is associated with
+ * @return the active editor provided it matches the given source viewer or null.
+ */
+ public static AndroidXmlEditor fromTextViewer(ITextViewer viewer) {
+ IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (wwin != null) {
+ // Try the active editor first.
+ IWorkbenchPage page = wwin.getActivePage();
+ if (page != null) {
+ IEditorPart editor = page.getActiveEditor();
+ if (editor instanceof AndroidXmlEditor) {
+ ISourceViewer ssviewer =
+ ((AndroidXmlEditor) editor).getStructuredSourceViewer();
+ if (ssviewer == viewer) {
+ return (AndroidXmlEditor) editor;
+ }
+ }
+ }
+
+ // If that didn't work, try all the editors
+ for (IWorkbenchPage page2 : wwin.getPages()) {
+ if (page2 != null) {
+ for (IEditorReference editorRef : page2.getEditorReferences()) {
+ IEditorPart editor = editorRef.getEditor(false /*restore*/);
+ if (editor instanceof AndroidXmlEditor) {
+ ISourceViewer ssviewer =
+ ((AndroidXmlEditor) editor).getStructuredSourceViewer();
+ if (ssviewer == viewer) {
+ return (AndroidXmlEditor) editor;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Called when this editor is activated */
+ public void activated() {
+ if (getActivePage() == mTextPageIndex) {
+ updateActionBindings();
+ }
+ }
+
+ /** Called when this editor is deactivated */
+ public void deactivated() {
+ }
+
+ /**
+ * Listen to changes in the underlying XML model in the structured editor.
+ */
+ private class XmlModelStateListener implements IModelStateListener {
+
+ /**
+ * A model is about to be changed. This typically is initiated by one
+ * client of the model, to signal a large change and/or a change to the
+ * model's ID or base Location. A typical use might be if a client might
+ * want to suspend processing until all changes have been made.
+ * <p/>
+ * This AndroidXmlEditor implementation of IModelChangedListener is empty.
+ */
+ @Override
+ public void modelAboutToBeChanged(IStructuredModel model) {
+ // pass
+ }
+
+ /**
+ * Signals that the changes foretold by modelAboutToBeChanged have been
+ * made. A typical use might be to refresh, or to resume processing that
+ * was suspended as a result of modelAboutToBeChanged.
+ * <p/>
+ * This AndroidXmlEditor implementation calls the xmlModelChanged callback.
+ */
+ @Override
+ public void modelChanged(IStructuredModel model) {
+ if (mIgnoreXmlUpdate) {
+ return;
+ }
+ xmlModelChanged(getXmlDocument(model));
+ }
+
+ /**
+ * Notifies that a model's dirty state has changed, and passes that state
+ * in isDirty. A model becomes dirty when any change is made, and becomes
+ * not-dirty when the model is saved.
+ * <p/>
+ * This AndroidXmlEditor implementation of IModelChangedListener is empty.
+ */
+ @Override
+ public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
+ // pass
+ }
+
+ /**
+ * A modelDeleted means the underlying resource has been deleted. The
+ * model itself is not removed from model management until all have
+ * released it. Note: baseLocation is not (necessarily) changed in this
+ * event, but may not be accurate.
+ * <p/>
+ * This AndroidXmlEditor implementation of IModelChangedListener is empty.
+ */
+ @Override
+ public void modelResourceDeleted(IStructuredModel model) {
+ // pass
+ }
+
+ /**
+ * A model has been renamed or copied (as in saveAs..). In the renamed
+ * case, the two parameters are the same instance, and only contain the
+ * new info for id and base location.
+ * <p/>
+ * This AndroidXmlEditor implementation of IModelChangedListener is empty.
+ */
+ @Override
+ public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
+ // pass
+ }
+
+ /**
+ * This AndroidXmlEditor implementation of IModelChangedListener is empty.
+ */
+ @Override
+ public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
+ // pass
+ }
+
+ /**
+ * This AndroidXmlEditor implementation of IModelChangedListener is empty.
+ */
+ @Override
+ public void modelReinitialized(IStructuredModel structuredModel) {
+ // pass
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java
new file mode 100644
index 000000000..2d4467799
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static com.android.SdkConstants.XMLNS;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.utils.XmlUtils;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.ISourceRange;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Just like {@link org.eclipse.jface.text.contentassist.CompletionProposal},
+ * but computes the documentation string lazily since they are typically only
+ * displayed for a small subset (the currently focused item) of the available
+ * proposals, and producing the strings requires some computation.
+ * <p>
+ * It also attempts to compute documentation for value strings like
+ * ?android:attr/dividerHeight.
+ * <p>
+ * TODO: Enhance this to compute documentation for additional values, such as
+ * the various enum values (which are available in the attrs.xml file, but not
+ * in the AttributeInfo objects for each enum value). To do this, I should
+ * basically keep around the maps computed by the attrs.xml parser.
+ */
+class CompletionProposal implements ICompletionProposal {
+ private static final Pattern ATTRIBUTE_PATTERN =
+ Pattern.compile("[@?]android:attr/(.*)"); //$NON-NLS-1$
+
+ private final AndroidContentAssist mAssist;
+ private final Object mChoice;
+ private final int mCursorPosition;
+ private int mReplacementOffset;
+ private final int mReplacementLength;
+ private final String mReplacementString;
+ private final Image mImage;
+ private final String mDisplayString;
+ private final IContextInformation mContextInformation;
+ private final String mNsPrefix;
+ private final String mNsUri;
+ private String mAdditionalProposalInfo;
+
+ CompletionProposal(AndroidContentAssist assist,
+ Object choice, String replacementString, int replacementOffset,
+ int replacementLength, int cursorPosition, Image image, String displayString,
+ IContextInformation contextInformation, String additionalProposalInfo,
+ String nsPrefix, String nsUri) {
+ assert replacementString != null;
+ assert replacementOffset >= 0;
+ assert replacementLength >= 0;
+ assert cursorPosition >= 0;
+
+ mAssist = assist;
+ mChoice = choice;
+ mCursorPosition = cursorPosition;
+ mReplacementOffset = replacementOffset;
+ mReplacementLength = replacementLength;
+ mReplacementString = replacementString;
+ mImage = image;
+ mDisplayString = displayString;
+ mContextInformation = contextInformation;
+ mAdditionalProposalInfo = additionalProposalInfo;
+ mNsPrefix = nsPrefix;
+ mNsUri = nsUri;
+ }
+
+ @Override
+ public Point getSelection(IDocument document) {
+ return new Point(mReplacementOffset + mCursorPosition, 0);
+ }
+
+ @Override
+ public IContextInformation getContextInformation() {
+ return mContextInformation;
+ }
+
+ @Override
+ public Image getImage() {
+ return mImage;
+ }
+
+ @Override
+ public String getDisplayString() {
+ if (mDisplayString != null) {
+ return mDisplayString;
+ }
+ return mReplacementString;
+ }
+
+ @Override
+ public String getAdditionalProposalInfo() {
+ if (mAdditionalProposalInfo == null) {
+ if (mChoice instanceof ElementDescriptor) {
+ String tooltip = ((ElementDescriptor)mChoice).getTooltip();
+ mAdditionalProposalInfo = DescriptorsUtils.formatTooltip(tooltip);
+ } else if (mChoice instanceof TextAttributeDescriptor) {
+ mAdditionalProposalInfo = ((TextAttributeDescriptor) mChoice).getTooltip();
+ } else if (mChoice instanceof String) {
+ // Try to produce it lazily for strings like @android
+ String value = (String) mChoice;
+ Matcher matcher = ATTRIBUTE_PATTERN.matcher(value);
+ if (matcher.matches()) {
+ String attrName = matcher.group(1);
+ AndroidTargetData data = mAssist.getEditor().getTargetData();
+ if (data != null) {
+ IDescriptorProvider descriptorProvider =
+ data.getDescriptorProvider(mAssist.getRootDescriptorId());
+ if (descriptorProvider != null) {
+ ElementDescriptor[] rootElementDescriptors =
+ descriptorProvider.getRootElementDescriptors();
+ for (ElementDescriptor elementDesc : rootElementDescriptors) {
+ for (AttributeDescriptor desc : elementDesc.getAttributes()) {
+ String name = desc.getXmlLocalName();
+ if (attrName.equals(name)) {
+ IAttributeInfo attributeInfo = desc.getAttributeInfo();
+ if (attributeInfo != null) {
+ return attributeInfo.getJavaDoc();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ }
+ } else if (mChoice instanceof IType) {
+ IType type = (IType) mChoice;
+ try {
+ ISourceRange javadocRange = type.getJavadocRange();
+ if (javadocRange != null && javadocRange.getLength() > 0) {
+ ISourceRange sourceRange = type.getSourceRange();
+ if (sourceRange != null) {
+ String source = type.getSource();
+ int start = javadocRange.getOffset() - sourceRange.getOffset();
+ int length = javadocRange.getLength();
+ String doc = source.substring(start, start + length);
+ return doc;
+ }
+ }
+ return type.getAttachedJavadoc(new NullProgressMonitor());
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ return mAdditionalProposalInfo;
+ }
+
+ @Override
+ public void apply(IDocument document) {
+ try {
+ Position position = new Position(mReplacementOffset);
+ document.addPosition(position);
+
+ // Ensure that the namespace is defined in the document
+ String prefix = mNsPrefix;
+ if (mNsUri != null && prefix != null) {
+ Document dom = DomUtilities.getDocument(mAssist.getEditor());
+ if (dom != null) {
+ Element root = dom.getDocumentElement();
+ if (root != null) {
+ // Is the namespace already defined?
+ boolean found = false;
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ String name = attribute.getName();
+ if (name.startsWith(XMLNS) && mNsUri.equals(attribute.getValue())) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ if (prefix.endsWith(":")) { //$NON-NLS-1$
+ prefix = prefix.substring(0, prefix.length() - 1);
+ }
+ XmlUtils.lookupNamespacePrefix(root, mNsUri, prefix, true);
+ }
+ }
+ }
+ }
+
+ mReplacementOffset = position.getOffset();
+ document.removePosition(position);
+ document.replace(mReplacementOffset, mReplacementLength, mReplacementString);
+ } catch (BadLocationException x) {
+ // ignore
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java
new file mode 100644
index 000000000..95cec47e6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java
@@ -0,0 +1,1893 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static com.android.SdkConstants.ANDROID_PKG;
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_ON_CLICK;
+import static com.android.SdkConstants.CLASS_ACTIVITY;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_DOCS;
+import static com.android.SdkConstants.FD_DOCS_REFERENCE;
+import static com.android.SdkConstants.FN_RESOURCE_BASE;
+import static com.android.SdkConstants.FN_RESOURCE_CLASS;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.TAG_RESOURCES;
+import static com.android.SdkConstants.TAG_STYLE;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VIEW;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE;
+import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
+import static com.android.xml.AndroidManifest.NODE_SERVICE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceUrl;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.ide.eclipse.adt.io.IFolderWrapper;
+import com.android.io.FileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.io.IAbstractFolder;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.apache.xerces.parsers.DOMParser;
+import org.apache.xerces.xni.Augmentations;
+import org.apache.xerces.xni.NamespaceContext;
+import org.apache.xerces.xni.QName;
+import org.apache.xerces.xni.XMLAttributes;
+import org.apache.xerces.xni.XMLLocator;
+import org.apache.xerces.xni.XNIException;
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.ICodeAssist;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
+import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
+import org.eclipse.jdt.internal.ui.text.JavaWordFinder;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.SelectionDispatchAction;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.MultiPageEditorPart;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Class containing hyperlink resolvers for XML and Java files to jump to associated
+ * resources -- Java Activity and Service classes, XML layout and string declarations,
+ * image drawables, etc.
+ */
+@SuppressWarnings("restriction")
+public class Hyperlinks {
+ private static final String CATEGORY = "category"; //$NON-NLS-1$
+ private static final String ACTION = "action"; //$NON-NLS-1$
+ private static final String PERMISSION = "permission"; //$NON-NLS-1$
+ private static final String USES_PERMISSION = "uses-permission"; //$NON-NLS-1$
+ private static final String CATEGORY_PKG_PREFIX = "android.intent.category."; //$NON-NLS-1$
+ private static final String ACTION_PKG_PREFIX = "android.intent.action."; //$NON-NLS-1$
+ private static final String PERMISSION_PKG_PREFIX = "android.permission."; //$NON-NLS-1$
+
+ private Hyperlinks() {
+ // Not instantiatable. This is a container class containing shared code
+ // for the various inner classes that are actual hyperlink resolvers.
+ }
+
+ /**
+ * Returns whether a string represents a valid fully qualified name for a view class.
+ * Does not check for existence.
+ */
+ @VisibleForTesting
+ static boolean isViewClassName(String name) {
+ int length = name.length();
+ if (length < 2 || name.indexOf('.') == -1) {
+ return false;
+ }
+
+ boolean lastWasDot = true;
+ for (int i = 0; i < length; i++) {
+ char c = name.charAt(i);
+ if (lastWasDot) {
+ if (!Character.isJavaIdentifierStart(c)) {
+ return false;
+ }
+ lastWasDot = false;
+ } else {
+ if (c == '.') {
+ lastWasDot = true;
+ } else if (!Character.isJavaIdentifierPart(c)) {
+ return false;
+ }
+ }
+ }
+
+ return !lastWasDot;
+ }
+
+ /** Determines whether the given attribute <b>name</b> is linkable */
+ private static boolean isAttributeNameLink(XmlContext context) {
+ // We could potentially allow you to link to builtin Android properties:
+ // ANDROID_URI.equals(attribute.getNamespaceURI())
+ // and then jump into the res/values/attrs.xml document that is available
+ // in the SDK data directory (path found via
+ // IAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES)).
+ //
+ // For now, we're not doing that.
+ //
+ // We could also allow to jump into custom attributes in custom view
+ // classes. Not yet implemented.
+
+ return false;
+ }
+
+ /** Determines whether the given attribute <b>value</b> is linkable */
+ private static boolean isAttributeValueLink(XmlContext context) {
+ // Everything else here is attribute based
+ Attr attribute = context.getAttribute();
+ if (attribute == null) {
+ return false;
+ }
+
+ if (isClassAttribute(context) || isOnClickAttribute(context)
+ || isManifestName(context) || isStyleAttribute(context)) {
+ return true;
+ }
+
+ String value = attribute.getValue();
+ if (value.startsWith(NEW_ID_PREFIX)) {
+ // It's a value -declaration-, nowhere else to jump
+ // (though we could consider jumping to the R-file; would that
+ // be helpful?)
+ return !ATTR_ID.equals(attribute.getLocalName());
+ }
+
+ ResourceUrl resource = ResourceUrl.parse(value);
+ if (resource != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Determines whether the given element <b>name</b> is linkable */
+ private static boolean isElementNameLink(XmlContext context) {
+ if (isClassElement(context)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this node/attribute pair corresponds to a manifest reference to
+ * an activity.
+ */
+ private static boolean isActivity(XmlContext context) {
+ // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump
+ // to it
+ Attr attribute = context.getAttribute();
+ String tagName = context.getElement().getTagName();
+ if (NODE_ACTIVITY.equals(tagName) && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this node/attribute pair corresponds to a manifest android:name reference
+ */
+ private static boolean isManifestName(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ if (attribute != null && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ if (getEditor() instanceof ManifestEditor) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Opens the declaration corresponding to an android:name reference in the
+ * AndroidManifest.xml file
+ */
+ private static boolean openManifestName(IProject project, XmlContext context) {
+ if (isActivity(context)) {
+ String fqcn = getActivityClassFqcn(context);
+ return AdtPlugin.openJavaClass(project, fqcn);
+ } else if (isService(context)) {
+ String fqcn = getServiceClassFqcn(context);
+ return AdtPlugin.openJavaClass(project, fqcn);
+ } else if (isBuiltinPermission(context)) {
+ String permission = context.getAttribute().getValue();
+ // Mutate something like android.permission.ACCESS_CHECKIN_PROPERTIES
+ // into relative doc url android/Manifest.permission.html#ACCESS_CHECKIN_PROPERTIES
+ assert permission.startsWith(PERMISSION_PKG_PREFIX);
+ String relative = "android/Manifest.permission.html#" //$NON-NLS-1$
+ + permission.substring(PERMISSION_PKG_PREFIX.length());
+
+ URL url = getDocUrl(relative);
+ if (url != null) {
+ AdtPlugin.openUrl(url);
+ return true;
+ } else {
+ return false;
+ }
+ } else if (isBuiltinIntent(context)) {
+ String intent = context.getAttribute().getValue();
+ // Mutate something like android.intent.action.MAIN into
+ // into relative doc url android/content/Intent.html#ACTION_MAIN
+ String relative;
+ if (intent.startsWith(ACTION_PKG_PREFIX)) {
+ relative = "android/content/Intent.html#ACTION_" //$NON-NLS-1$
+ + intent.substring(ACTION_PKG_PREFIX.length());
+ } else if (intent.startsWith(CATEGORY_PKG_PREFIX)) {
+ relative = "android/content/Intent.html#CATEGORY_" //$NON-NLS-1$
+ + intent.substring(CATEGORY_PKG_PREFIX.length());
+ } else {
+ return false;
+ }
+ URL url = getDocUrl(relative);
+ if (url != null) {
+ AdtPlugin.openUrl(url);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /** Returns true if this represents a style attribute */
+ private static boolean isStyleAttribute(XmlContext context) {
+ String tag = context.getElement().getTagName();
+ return TAG_STYLE.equals(tag);
+ }
+
+ /**
+ * Returns true if this represents a {@code <view class="foo.bar.Baz">} class
+ * attribute, or a {@code <fragment android:name="foo.bar.Baz">} class attribute
+ */
+ private static boolean isClassAttribute(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ if (attribute == null) {
+ return false;
+ }
+ String tag = context.getElement().getTagName();
+ String attributeName = attribute.getLocalName();
+ return ATTR_CLASS.equals(attributeName) && (VIEW.equals(tag) || VIEW_FRAGMENT.equals(tag))
+ || ATTR_NAME.equals(attributeName) && VIEW_FRAGMENT.equals(tag)
+ || (ATTR_CONTEXT.equals(attributeName)
+ && TOOLS_URI.equals(attribute.getNamespaceURI()));
+ }
+
+ /** Returns true if this represents an onClick attribute specifying a method handler */
+ private static boolean isOnClickAttribute(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ if (attribute == null) {
+ return false;
+ }
+ return ATTR_ON_CLICK.equals(attribute.getLocalName()) && attribute.getValue().length() > 0;
+ }
+
+ /** Returns true if this represents a {@code <foo.bar.Baz>} custom view class element */
+ private static boolean isClassElement(XmlContext context) {
+ if (context.getAttribute() != null) {
+ // Don't match the outer element if the user is hovering over a specific attribute
+ return false;
+ }
+ // If the element looks like a fully qualified class name (e.g. it's a custom view
+ // element) offer it as a link
+ String tag = context.getElement().getTagName();
+ return isViewClassName(tag);
+ }
+
+ /** Returns the FQCN for a class declaration at the given context */
+ private static String getClassFqcn(XmlContext context) {
+ if (isClassAttribute(context)) {
+ String value = context.getAttribute().getValue();
+ if (!value.isEmpty() && value.charAt(0) == '.') {
+ IProject project = getProject();
+ if (project != null) {
+ ManifestInfo info = ManifestInfo.get(project);
+ String pkg = info.getPackage();
+ if (pkg != null) {
+ value = pkg + value;
+ }
+ }
+ }
+ return value;
+ } else if (isClassElement(context)) {
+ return context.getElement().getTagName();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if this node/attribute pair corresponds to a manifest reference to
+ * an service.
+ */
+ private static boolean isService(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ Element node = context.getElement();
+
+ // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
+ String nodeName = node.getNodeName();
+ if (NODE_SERVICE.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a URL pointing to the Android reference documentation, either installed
+ * locally or the one on android.com
+ *
+ * @param relative a relative url to append to the root url
+ * @return a URL pointing to the documentation
+ */
+ private static URL getDocUrl(String relative) {
+ // First try to find locally installed documentation
+ File sdkLocation = new File(Sdk.getCurrent().getSdkOsLocation());
+ File docs = new File(sdkLocation, FD_DOCS + File.separator + FD_DOCS_REFERENCE);
+ try {
+ if (docs.exists()) {
+ String s = docs.toURI().toURL().toExternalForm();
+ if (!s.endsWith("/")) { //$NON-NLS-1$
+ s += "/"; //$NON-NLS-1$
+ }
+ return new URL(s + relative);
+ }
+ // If not, fallback to the online documentation
+ return new URL("http://developer.android.com/reference/" + relative); //$NON-NLS-1$
+ } catch (MalformedURLException e) {
+ AdtPlugin.log(e, "Can't create URL for %1$s", docs);
+ return null;
+ }
+ }
+
+ /** Returns true if the context is pointing to a permission name reference */
+ private static boolean isBuiltinPermission(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ Element node = context.getElement();
+
+ // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
+ String nodeName = node.getNodeName();
+ if ((USES_PERMISSION.equals(nodeName) || PERMISSION.equals(nodeName))
+ && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ String value = attribute.getValue();
+ if (value.startsWith(PERMISSION_PKG_PREFIX)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Returns true if the context is pointing to an intent reference */
+ private static boolean isBuiltinIntent(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ Element node = context.getElement();
+
+ // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it
+ String nodeName = node.getNodeName();
+ if ((ACTION.equals(nodeName) || CATEGORY.equals(nodeName))
+ && ATTRIBUTE_NAME.equals(attribute.getLocalName())
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ String value = attribute.getValue();
+ if (value.startsWith(ACTION_PKG_PREFIX) || value.startsWith(CATEGORY_PKG_PREFIX)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Returns the fully qualified class name of an activity referenced by the given
+ * AndroidManifest.xml node
+ */
+ private static String getActivityClassFqcn(XmlContext context) {
+ Attr attribute = context.getAttribute();
+ Element node = context.getElement();
+ StringBuilder sb = new StringBuilder();
+ Element root = node.getOwnerDocument().getDocumentElement();
+ String pkg = root.getAttribute(ATTRIBUTE_PACKAGE);
+ String className = attribute.getValue();
+ if (className.startsWith(".")) { //$NON-NLS-1$
+ sb.append(pkg);
+ } else if (className.indexOf('.') == -1) {
+ // According to the <activity> manifest element documentation, this is not
+ // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html )
+ // but it appears in manifest files and appears to be supported by the runtime
+ // so handle this in code as well:
+ sb.append(pkg);
+ sb.append('.');
+ } // else: the class name is already a fully qualified class name
+ sb.append(className);
+ return sb.toString();
+ }
+
+ /**
+ * Returns the fully qualified class name of a service referenced by the given
+ * AndroidManifest.xml node
+ */
+ private static String getServiceClassFqcn(XmlContext context) {
+ // Same logic
+ return getActivityClassFqcn(context);
+ }
+
+ /**
+ * Returns the XML tag containing an element description for value items of the given
+ * resource type
+ *
+ * @param type the resource type to query the XML tag name for
+ * @return the tag name used for value declarations in XML of resources of the given
+ * type
+ */
+ public static String getTagName(ResourceType type) {
+ if (type == ResourceType.ID) {
+ // Ids are recorded in <item> tags instead of <id> tags
+ return SdkConstants.TAG_ITEM;
+ }
+
+ return type.getName();
+ }
+
+ /**
+ * Computes the actual exact location to jump to for a given XML context.
+ *
+ * @param context the XML context to be opened
+ * @return true if the request was handled successfully
+ */
+ private static boolean open(XmlContext context) {
+ IProject project = getProject();
+ if (project == null) {
+ return false;
+ }
+
+ if (isManifestName(context)) {
+ return openManifestName(project, context);
+ } else if (isClassElement(context) || isClassAttribute(context)) {
+ return AdtPlugin.openJavaClass(project, getClassFqcn(context));
+ } else if (isOnClickAttribute(context)) {
+ return openOnClickMethod(project, context.getAttribute().getValue());
+ } else {
+ return false;
+ }
+ }
+
+ /** Opens a path (which may not be in the workspace) */
+ private static void openPath(IPath filePath, IRegion region, int offset) {
+ IEditorPart sourceEditor = getEditor();
+ IWorkbenchPage page = sourceEditor.getEditorSite().getPage();
+
+ IFile file = AdtUtils.pathToIFile(filePath);
+ if (file != null && file.exists()) {
+ try {
+ AdtPlugin.openFile(file, region);
+ return;
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$
+ }
+ } else {
+ // It's not a path in the workspace; look externally
+ // (this is probably an @android: path)
+ if (filePath.isAbsolute()) {
+ IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath);
+ if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) {
+ try {
+ IEditorPart target = IDE.openEditorOnFileStore(page, fileStore);
+ if (target instanceof MultiPageEditorPart) {
+ MultiPageEditorPart part = (MultiPageEditorPart) target;
+ IEditorPart[] editors = part.findEditors(target.getEditorInput());
+ if (editors != null) {
+ for (IEditorPart editor : editors) {
+ if (editor instanceof StructuredTextEditor) {
+ StructuredTextEditor ste = (StructuredTextEditor) editor;
+ part.setActiveEditor(editor);
+ ste.selectAndReveal(offset, 0);
+ break;
+ }
+ }
+ }
+ }
+
+ return;
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ // Failed: display message to the user
+ displayError(String.format("Could not find resource %1$s", filePath));
+ }
+
+ private static void displayError(String message) {
+ // Failed: display message to the user
+ IEditorSite editorSite = getEditor().getEditorSite();
+ IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
+ status.setErrorMessage(message);
+ }
+
+ /**
+ * Opens a Java method referenced by the given on click attribute method name
+ *
+ * @param project the project containing the click handler
+ * @param method the method name of the on click handler
+ * @return true if the method was opened, false otherwise
+ */
+ public static boolean openOnClickMethod(IProject project, String method) {
+ // Search for the method in the Java index, filtering by the required click handler
+ // method signature (public and has a single View parameter), and narrowing the scope
+ // first to Activity classes, then to the whole workspace.
+ final AtomicBoolean success = new AtomicBoolean(false);
+ SearchRequestor requestor = new SearchRequestor() {
+ @Override
+ public void acceptSearchMatch(SearchMatch match) throws CoreException {
+ Object element = match.getElement();
+ if (element instanceof IMethod) {
+ IMethod methodElement = (IMethod) element;
+ String[] parameterTypes = methodElement.getParameterTypes();
+ if (parameterTypes != null
+ && parameterTypes.length == 1
+ && ("Qandroid.view.View;".equals(parameterTypes[0]) //$NON-NLS-1$
+ || "QView;".equals(parameterTypes[0]))) { //$NON-NLS-1$
+ // Check that it's public
+ if (Flags.isPublic(methodElement.getFlags())) {
+ JavaUI.openInEditor(methodElement);
+ success.getAndSet(true);
+ }
+ }
+ }
+ }
+ };
+ try {
+ IJavaSearchScope scope = null;
+ IType activityType = null;
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ activityType = javaProject.findType(CLASS_ACTIVITY);
+ if (activityType != null) {
+ scope = SearchEngine.createHierarchyScope(activityType);
+ }
+ }
+ if (scope == null) {
+ scope = SearchEngine.createWorkspaceScope();
+ }
+
+ SearchParticipant[] participants = new SearchParticipant[] {
+ SearchEngine.getDefaultSearchParticipant()
+ };
+ int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
+ SearchPattern pattern = SearchPattern.createPattern("*." + method,
+ IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, matchRule);
+ SearchEngine engine = new SearchEngine();
+ engine.search(pattern, participants, scope, requestor, new NullProgressMonitor());
+
+ boolean ok = success.get();
+ if (!ok && activityType != null) {
+ // TODO: Create a project+dependencies scope and search only that scope
+
+ // Try searching again with a complete workspace scope this time
+ scope = SearchEngine.createWorkspaceScope();
+ engine.search(pattern, participants, scope, requestor, new NullProgressMonitor());
+
+ // TODO: There could be more than one match; add code to consider them all
+ // and pick the most likely candidate and open only that one.
+
+ ok = success.get();
+ }
+ return ok;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the current configuration, if the associated UI editor has been initialized
+ * and has an associated configuration
+ *
+ * @return the configuration for this file, or null
+ */
+ private static FolderConfiguration getConfiguration() {
+ IEditorPart editor = getEditor();
+ if (editor != null) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
+ GraphicalEditorPart graphicalEditor =
+ delegate == null ? null : delegate.getGraphicalEditor();
+
+ if (graphicalEditor != null) {
+ return graphicalEditor.getConfiguration();
+ } else {
+ // TODO: Could try a few more things to get the configuration:
+ // (1) try to look at the file.getPersistentProperty(NAME_CONFIG_STATE)
+ // which will return previously saved state. This isn't necessary today
+ // since no editors seem to be lazily initialized.
+ // (2) attempt to use the configuration from any of the other open
+ // files, especially files in the same directory as this one.
+ }
+
+ // Create a configuration from the current file
+ IProject project = null;
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ IFile file = ((FileEditorInput) editorInput).getFile();
+ project = file.getProject();
+ ProjectResources pr = ResourceManager.getInstance().getProjectResources(project);
+ IContainer parent = file.getParent();
+ if (parent instanceof IFolder) {
+ ResourceFolder resFolder = pr.getResourceFolder((IFolder) parent);
+ if (resFolder != null) {
+ return resFolder.getConfiguration();
+ }
+ }
+ }
+
+ // Might be editing a Java file, where there is no configuration context.
+ // Instead look at surrounding files in the workspace and obtain one valid
+ // configuration.
+ for (IEditorReference reference : editor.getSite().getPage().getEditorReferences()) {
+ IEditorPart part = reference.getEditor(false /*restore*/);
+
+ LayoutEditorDelegate refDelegate = LayoutEditorDelegate.fromEditor(part);
+ if (refDelegate != null) {
+ IProject refProject = refDelegate.getEditor().getProject();
+ if (project == null || project == refProject) {
+ GraphicalEditorPart refGraphicalEditor = refDelegate.getGraphicalEditor();
+ if (refGraphicalEditor != null) {
+ return refGraphicalEditor.getConfiguration();
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the {@link IAndroidTarget} to be used for looking up system resources */
+ private static IAndroidTarget getTarget(IProject project) {
+ IEditorPart editor = getEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
+ if (delegate != null) {
+ GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor();
+ if (graphicalEditor != null) {
+ return graphicalEditor.getRenderingTarget();
+ }
+ }
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk == null) {
+ return null;
+ }
+
+ return currentSdk.getTarget(project);
+ }
+
+ /** Return either the project resources or the framework resources (or null) */
+ private static ResourceRepository getResources(IProject project, boolean framework) {
+ if (framework) {
+ IAndroidTarget target = getTarget(project);
+
+ if (target == null && project == null && framework) {
+ // No current project: probably jumped into some of the framework XML resource
+ // files and attempting to jump around. Attempt to figure out which target
+ // we're dealing with and continue looking within the same framework.
+ IEditorPart editor = getEditor();
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null && editor instanceof AndroidXmlEditor) {
+ AndroidTargetData data = ((AndroidXmlEditor) editor).getTargetData();
+ if (data != null) {
+ return data.getFrameworkResources();
+ }
+ }
+ }
+
+ if (target == null) {
+ return null;
+ }
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ if (data == null) {
+ return null;
+ }
+ return data.getFrameworkResources();
+ } else {
+ return ResourceManager.getInstance().getProjectResources(project);
+ }
+ }
+
+ /**
+ * Finds a definition of an id attribute in layouts. (Ids can also be defined as
+ * resources; use {@link #findValueInXml} or {@link #findValueInDocument} to locate it there.)
+ */
+ private static Pair<IFile, IRegion> findIdDefinition(IProject project, String id) {
+ // FIRST look in the same file as the originating request, that's where you usually
+ // want to jump
+ IFile self = AdtUtils.getActiveFile();
+ if (self != null && EXT_XML.equals(self.getFileExtension())) {
+ Pair<IFile, IRegion> target = findIdInXml(id, self);
+ if (target != null) {
+ return target;
+ }
+ }
+
+ // Look in the configuration folder: Search compatible configurations
+ ResourceRepository resources = getResources(project, false /* isFramework */);
+ FolderConfiguration configuration = getConfiguration();
+ if (configuration != null) { // Not the case when searching from Java files for example
+ List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT);
+ if (folders != null) {
+ for (ResourceFolder folder : folders) {
+ if (folder.getConfiguration().isMatchFor(configuration)) {
+ IAbstractFolder wrapper = folder.getFolder();
+ if (wrapper instanceof IFolderWrapper) {
+ IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder();
+ Pair<IFile, IRegion> target = findIdInFolder(iFolder, id);
+ if (target != null) {
+ return target;
+ }
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ // Ugh. Search ALL layout files in the project!
+ List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT);
+ if (folders != null) {
+ for (ResourceFolder folder : folders) {
+ IAbstractFolder wrapper = folder.getFolder();
+ if (wrapper instanceof IFolderWrapper) {
+ IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder();
+ Pair<IFile, IRegion> target = findIdInFolder(iFolder, id);
+ if (target != null) {
+ return target;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds a definition of an id attribute in a particular layout folder.
+ */
+ private static Pair<IFile, IRegion> findIdInFolder(IContainer f, String id) {
+ try {
+ // Check XML files in values/
+ for (IResource resource : f.members()) {
+ if (resource.exists() && !resource.isDerived() && resource instanceof IFile) {
+ IFile file = (IFile) resource;
+ // Must have an XML extension
+ if (EXT_XML.equals(file.getFileExtension())) {
+ Pair<IFile, IRegion> target = findIdInXml(id, file);
+ if (target != null) {
+ return target;
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, ""); //$NON-NLS-1$
+ }
+
+ return null;
+ }
+
+ /** Parses the given file and locates a definition of the given resource */
+ private static Pair<IFile, IRegion> findValueInXml(
+ ResourceType type, String name, IFile file) {
+ IStructuredModel model = null;
+ try {
+ model = StructuredModelManager.getModelManager().getExistingModelForRead(file);
+ if (model == null) {
+ // There is no open or cached model for the file; see if the file looks
+ // like it's interesting (content contains the String name we are looking for)
+ if (AdtPlugin.fileContains(file, name)) {
+ // Yes, so parse content
+ model = StructuredModelManager.getModelManager().getModelForRead(file);
+ }
+ }
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ return findValueInDocument(type, name, file, document);
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return null;
+ }
+
+ /** Looks within an XML DOM document for the given resource name and returns it */
+ private static Pair<IFile, IRegion> findValueInDocument(
+ ResourceType type, String name, IFile file, Document document) {
+ String targetTag = getTagName(type);
+ Element root = document.getDocumentElement();
+ if (root.getTagName().equals(TAG_RESOURCES)) {
+ NodeList topLevel = root.getChildNodes();
+ Pair<IFile, IRegion> value = findValueInChildren(name, file, targetTag, topLevel);
+ if (value == null && type == ResourceType.ATTR) {
+ for (int i = 0, n = topLevel.getLength(); i < n; i++) {
+ Node child = topLevel.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element)child;
+ String tagName = element.getTagName();
+ if (tagName.equals("declare-styleable")) {
+ NodeList children = element.getChildNodes();
+ value = findValueInChildren(name, file, targetTag, children);
+ if (value != null) {
+ return value;
+ }
+ }
+ }
+ }
+ }
+
+ return value;
+ }
+
+ return null;
+ }
+
+ private static Pair<IFile, IRegion> findValueInChildren(String name, IFile file,
+ String targetTag, NodeList children) {
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element)child;
+ String tagName = element.getTagName();
+ if (tagName.equals(targetTag)) {
+ String elementName = element.getAttribute(ATTR_NAME);
+ if (elementName.equals(name)) {
+ IRegion region = null;
+ if (element instanceof IndexedRegion) {
+ IndexedRegion r = (IndexedRegion) element;
+ // IndexedRegion.getLength() returns bogus values
+ int length = r.getEndOffset() - r.getStartOffset();
+ region = new Region(r.getStartOffset(), length);
+ }
+
+ return Pair.of(file, region);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Parses the given file and locates a definition of the given resource */
+ private static Pair<IFile, IRegion> findIdInXml(String id, IFile file) {
+ IStructuredModel model = null;
+ try {
+ model = StructuredModelManager.getModelManager().getExistingModelForRead(file);
+ if (model == null) {
+ // There is no open or cached model for the file; see if the file looks
+ // like it's interesting (content contains the String name we are looking for)
+ if (AdtPlugin.fileContains(file, id)) {
+ // Yes, so parse content
+ model = StructuredModelManager.getModelManager().getModelForRead(file);
+ }
+ }
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ return findIdInDocument(id, file, document);
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return null;
+ }
+
+ /** Looks within an XML DOM document for the given resource name and returns it */
+ private static Pair<IFile, IRegion> findIdInDocument(String id, IFile file,
+ Document document) {
+ String targetAttribute = NEW_ID_PREFIX + id;
+ Element root = document.getDocumentElement();
+ Pair<IFile, IRegion> result = findIdInElement(root, file, targetAttribute,
+ true /*requireId*/);
+ if (result == null) {
+ result = findIdInElement(root, file, targetAttribute, false /*requireId*/);
+ }
+ return result;
+ }
+
+ private static Pair<IFile, IRegion> findIdInElement(
+ Element root, IFile file, String targetAttribute, boolean requireIdAttribute) {
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node item = attributes.item(i);
+ if (item instanceof Attr) {
+ Attr attribute = (Attr) item;
+ if (requireIdAttribute && !ATTR_ID.equals(attribute.getLocalName())) {
+ continue;
+ }
+ String value = attribute.getValue();
+ if (value.equals(targetAttribute)) {
+ // Select the element -containing- the id rather than the attribute itself
+ IRegion region = null;
+ Node element = attribute.getOwnerElement();
+ //if (attribute instanceof IndexedRegion) {
+ if (element instanceof IndexedRegion) {
+ IndexedRegion r = (IndexedRegion) element;
+ int length = r.getEndOffset() - r.getStartOffset();
+ region = new Region(r.getStartOffset(), length);
+ }
+
+ return Pair.of(file, region);
+ }
+ }
+ }
+
+ NodeList children = root.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element)child;
+ Pair<IFile, IRegion> result = findIdInElement(element, file, targetAttribute,
+ requireIdAttribute);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Parses the given file and locates a definition of the given resource */
+ private static Pair<File, Integer> findValueInXml(ResourceType type, String name, File file) {
+ // We can't use the StructureModelManager on files outside projects
+ // There is no open or cached model for the file; see if the file looks
+ // like it's interesting (content contains the String name we are looking for)
+ if (AdtPlugin.fileContains(file, name)) {
+ try {
+ InputSource is = new InputSource(new FileInputStream(file));
+ OffsetTrackingParser parser = new OffsetTrackingParser();
+ parser.parse(is);
+ Document document = parser.getDocument();
+
+ return findValueInDocument(type, name, file, parser, document);
+ } catch (SAXException e) {
+ // pass -- ignore files we can't parse
+ } catch (IOException e) {
+ // pass -- ignore files we can't parse
+ }
+ }
+
+ return null;
+ }
+
+ /** Looks within an XML DOM document for the given resource name and returns it */
+ private static Pair<File, Integer> findValueInDocument(ResourceType type, String name,
+ File file, OffsetTrackingParser parser, Document document) {
+ String targetTag = type.getName();
+ if (type == ResourceType.ID) {
+ // Ids are recorded in <item> tags instead of <id> tags
+ targetTag = "item"; //$NON-NLS-1$
+ }
+
+ Pair<File, Integer> result = findTag(name, file, parser, document, targetTag);
+ if (result == null && type == ResourceType.ATTR) {
+ // Attributes seem to be defined in <public> tags
+ targetTag = "public"; //$NON-NLS-1$
+ result = findTag(name, file, parser, document, targetTag);
+ }
+ return result;
+ }
+
+ private static Pair<File, Integer> findTag(String name, File file, OffsetTrackingParser parser,
+ Document document, String targetTag) {
+ NodeList children = document.getElementsByTagName(targetTag);
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) child;
+ if (element.getTagName().equals(targetTag)) {
+ String elementName = element.getAttribute(ATTR_NAME);
+ if (elementName.equals(name)) {
+ return Pair.of(file, parser.getOffset(element));
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static IHyperlink[] getStyleLinks(XmlContext context, IRegion range, String url) {
+ Attr attribute = context.getAttribute();
+ if (attribute != null) {
+ // Split up theme resource urls to the nearest dot forwards, such that you
+ // can point to "Theme.Light" by placing the caret anywhere after the dot,
+ // and point to just "Theme" by pointing before it.
+ int caret = context.getInnerRegionCaretOffset();
+ String value = attribute.getValue();
+ int index = value.indexOf('.', caret);
+ if (index != -1) {
+ url = url.substring(0, index);
+ range = new Region(range.getOffset(),
+ range.getLength() - (value.length() - index));
+ }
+ }
+
+ ResourceUrl resource = ResourceUrl.parse(url);
+ if (resource == null) {
+ String androidStyle = ANDROID_STYLE_RESOURCE_PREFIX;
+ if (url.startsWith(ANDROID_PREFIX)) {
+ url = androidStyle + url.substring(ANDROID_PREFIX.length());
+ } else if (url.startsWith(ANDROID_THEME_PREFIX)) {
+ url = androidStyle + url.substring(ANDROID_THEME_PREFIX.length());
+ } else if (url.startsWith(ANDROID_PKG + ':')) {
+ url = androidStyle + url.substring(ANDROID_PKG.length() + 1);
+ } else {
+ url = STYLE_RESOURCE_PREFIX + url;
+ }
+ }
+ return getResourceLinks(range, url);
+ }
+
+ private static IHyperlink[] getResourceLinks(@Nullable IRegion range, @NonNull String url) {
+ IProject project = Hyperlinks.getProject();
+ FolderConfiguration configuration = getConfiguration();
+ return getResourceLinks(range, url, project, configuration);
+ }
+
+ /**
+ * Computes hyperlinks to resource definitions for resource urls (e.g.
+ * {@code @android:string/ok} or {@code @layout/foo}. May create multiple links.
+ * @param range TBD
+ * @param url the resource url
+ * @param project the relevant project
+ * @param configuration the applicable configuration
+ * @return an array of hyperlinks, or null
+ */
+ @Nullable
+ public static IHyperlink[] getResourceLinks(@Nullable IRegion range, @NonNull String url,
+ @Nullable IProject project, @Nullable FolderConfiguration configuration) {
+ List<IHyperlink> links = new ArrayList<IHyperlink>();
+
+ ResourceUrl resource = ResourceUrl.parse(url);
+ if (resource == null) {
+ return null;
+ }
+ ResourceType type = resource.type;
+ String name = resource.name;
+ boolean isFramework = resource.framework;
+ if (project == null) {
+ // Local reference *within* a framework
+ isFramework = true;
+ }
+
+ ResourceRepository resources = getResources(project, isFramework);
+ if (resources == null) {
+ return null;
+ }
+ List<ResourceFile> sourceFiles = resources.getSourceFiles(type, name,
+ null /*configuration*/);
+ if (sourceFiles == null) {
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null) {
+ List<IProject> libraries = projectState.getFullLibraryProjects();
+ if (libraries != null && !libraries.isEmpty()) {
+ for (IProject library : libraries) {
+ resources = ResourceManager.getInstance().getProjectResources(library);
+ sourceFiles = resources.getSourceFiles(type, name, null /*configuration*/);
+ if (sourceFiles != null && !sourceFiles.isEmpty()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ ResourceFile best = null;
+ if (configuration != null && sourceFiles != null && sourceFiles.size() > 0) {
+ List<ResourceFile> bestFiles = resources.getSourceFiles(type, name, configuration);
+ if (bestFiles != null && bestFiles.size() > 0) {
+ best = bestFiles.get(0);
+ }
+ }
+ if (sourceFiles != null) {
+ List<ResourceFile> matches = new ArrayList<ResourceFile>();
+ for (ResourceFile resourceFile : sourceFiles) {
+ matches.add(resourceFile);
+ }
+
+ if (matches.size() > 0) {
+ final ResourceFile fBest = best;
+ Collections.sort(matches, new Comparator<ResourceFile>() {
+ @Override
+ public int compare(ResourceFile rf1, ResourceFile rf2) {
+ // Sort best item to the front
+ if (rf1 == fBest) {
+ return -1;
+ } else if (rf2 == fBest) {
+ return 1;
+ } else {
+ return getFileName(rf1).compareTo(getFileName(rf2));
+ }
+ }
+ });
+
+ // Is this something found in a values/ folder?
+ boolean valueResource = ResourceHelper.isValueBasedResourceType(type);
+
+ for (ResourceFile file : matches) {
+ String folderName = file.getFolder().getFolder().getName();
+ String label = String.format("Open Declaration in %1$s/%2$s",
+ folderName, getFileName(file));
+
+ // Only search for resource type within the file if it's an
+ // XML file and it is a value resource
+ ResourceLink link = new ResourceLink(label, range, file,
+ valueResource ? type : null, name);
+ links.add(link);
+ }
+ }
+ }
+
+ // Id's are handled specially because they are typically defined
+ // inline (though they -can- be defined in the values folder above as
+ // well, in which case we will prefer that definition)
+ if (!isFramework && type == ResourceType.ID && links.size() == 0) {
+ // Must compute these lazily...
+ links.add(new ResourceLink("Open XML Declaration", range, null, type, name));
+ }
+
+ if (links.size() > 0) {
+ return links.toArray(new IHyperlink[links.size()]);
+ } else {
+ return null;
+ }
+ }
+
+ private static String getFileName(ResourceFile file) {
+ return file.getFile().getName();
+ }
+
+ /** Detector for finding Android references in XML files */
+ public static class XmlResolver extends AbstractHyperlinkDetector {
+
+ @Override
+ public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
+ boolean canShowMultipleHyperlinks) {
+
+ if (region == null || textViewer == null) {
+ return null;
+ }
+
+ IDocument document = textViewer.getDocument();
+
+ XmlContext context = XmlContext.find(document, region.getOffset());
+ if (context == null) {
+ return null;
+ }
+
+ IRegion range = context.getInnerRange(document);
+ boolean isLinkable = false;
+ String type = context.getInnerRegion().getType();
+ if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE) {
+ if (isAttributeValueLink(context)) {
+ isLinkable = true;
+ // Strip out quotes
+ range = new Region(range.getOffset() + 1, range.getLength() - 2);
+
+ Attr attribute = context.getAttribute();
+ if (isStyleAttribute(context)) {
+ return getStyleLinks(context, range, attribute.getValue());
+ }
+ if (attribute != null
+ && (attribute.getValue().startsWith(PREFIX_RESOURCE_REF)
+ || attribute.getValue().startsWith(PREFIX_THEME_REF))) {
+ // Instantly create links for resources since we can use the existing
+ // resolved maps for this and offer multiple choices for the user
+ String url = attribute.getValue();
+ return getResourceLinks(range, url);
+ }
+ }
+ } else if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) {
+ if (isAttributeNameLink(context)) {
+ isLinkable = true;
+ }
+ } else if (type == DOMRegionContext.XML_TAG_NAME) {
+ if (isElementNameLink(context)) {
+ isLinkable = true;
+ }
+ } else if (type == DOMRegionContext.XML_CONTENT) {
+ Node parentNode = context.getNode().getParentNode();
+ if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) {
+ // Try to complete resources defined inline as text, such as
+ // style definitions
+ ITextRegion outer = context.getElementRegion();
+ ITextRegion inner = context.getInnerRegion();
+ int innerOffset = outer.getStart() + inner.getStart();
+ int caretOffset = innerOffset + context.getInnerRegionCaretOffset();
+ try {
+ IRegion lineInfo = document.getLineInformationOfOffset(caretOffset);
+ int lineStart = lineInfo.getOffset();
+ int lineEnd = Math.min(lineStart + lineInfo.getLength(),
+ innerOffset + inner.getLength());
+
+ // Compute the resource URL
+ int urlStart = -1;
+ int offset = caretOffset;
+ while (offset > lineStart) {
+ char c = document.getChar(offset);
+ if (c == '@' || c == '?') {
+ urlStart = offset;
+ break;
+ } else if (!isValidResourceUrlChar(c)) {
+ break;
+ }
+ offset--;
+ }
+
+ if (urlStart != -1) {
+ offset = caretOffset;
+ while (offset < lineEnd) {
+ if (!isValidResourceUrlChar(document.getChar(offset))) {
+ break;
+ }
+ offset++;
+ }
+
+ int length = offset - urlStart;
+ String url = document.get(urlStart, length);
+ range = new Region(urlStart, length);
+ return getResourceLinks(range, url);
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ if (isLinkable) {
+ IHyperlink hyperlink = new DeferredResolutionLink(context, range);
+ if (hyperlink != null) {
+ return new IHyperlink[] {
+ hyperlink
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private static boolean isValidResourceUrlChar(char c) {
+ return Character.isJavaIdentifierPart(c) || c == ':' || c == '/' || c == '.' || c == '+';
+
+ }
+
+ /** Detector for finding Android references in Java files */
+ public static class JavaResolver extends AbstractHyperlinkDetector {
+
+ @Override
+ public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
+ boolean canShowMultipleHyperlinks) {
+ // Most of this is identical to the builtin JavaElementHyperlinkDetector --
+ // everything down to the Android R filtering below
+
+ ITextEditor textEditor = (ITextEditor) getAdapter(ITextEditor.class);
+ if (region == null || !(textEditor instanceof JavaEditor))
+ return null;
+
+ IAction openAction = textEditor.getAction("OpenEditor"); //$NON-NLS-1$
+ if (!(openAction instanceof SelectionDispatchAction))
+ return null;
+
+ int offset = region.getOffset();
+
+ IJavaElement input = EditorUtility.getEditorInputJavaElement(textEditor, false);
+ if (input == null)
+ return null;
+
+ try {
+ IDocument document = textEditor.getDocumentProvider().getDocument(
+ textEditor.getEditorInput());
+ IRegion wordRegion = JavaWordFinder.findWord(document, offset);
+ if (wordRegion == null || wordRegion.getLength() == 0)
+ return null;
+
+ IJavaElement[] elements = null;
+ elements = ((ICodeAssist) input).codeSelect(wordRegion.getOffset(), wordRegion
+ .getLength());
+
+ // Specific Android R class filtering:
+ if (elements.length > 0) {
+ IJavaElement element = elements[0];
+ if (element.getElementType() == IJavaElement.FIELD) {
+ IJavaElement unit = element.getAncestor(IJavaElement.COMPILATION_UNIT);
+ if (unit == null) {
+ // Probably in a binary; see if this is an android.R resource
+ IJavaElement type = element.getAncestor(IJavaElement.TYPE);
+ if (type != null && type.getParent() != null) {
+ IJavaElement parentType = type.getParent();
+ if (parentType.getElementType() == IJavaElement.CLASS_FILE) {
+ String pn = parentType.getElementName();
+ String prefix = FN_RESOURCE_BASE + "$"; //$NON-NLS-1$
+ if (pn.startsWith(prefix)) {
+ return createTypeLink(element, type, wordRegion, true);
+ }
+ }
+ }
+ } else if (FN_RESOURCE_CLASS.equals(unit.getElementName())) {
+ // Yes, we're referencing the project R class.
+ // Offer hyperlink navigation to XML resource files for
+ // the various definitions
+ IJavaElement type = element.getAncestor(IJavaElement.TYPE);
+ if (type != null) {
+ return createTypeLink(element, type, wordRegion, false);
+ }
+ }
+ }
+
+ }
+ return null;
+ } catch (JavaModelException e) {
+ return null;
+ }
+ }
+
+ private IHyperlink[] createTypeLink(IJavaElement element, IJavaElement type,
+ IRegion wordRegion, boolean isFrameworkResource) {
+ String typeName = type.getElementName();
+ // typeName will be "id", "layout", "string", etc
+ if (isFrameworkResource) {
+ typeName = ANDROID_PKG + ':' + typeName;
+ }
+ String elementName = element.getElementName();
+ String url = '@' + typeName + '/' + elementName;
+ return getResourceLinks(wordRegion, url);
+ }
+ }
+
+ /** Returns the editor applicable to this hyperlink detection */
+ private static IEditorPart getEditor() {
+ // I would like to be able to find this via getAdapter(TextEditor.class) but
+ // couldn't find a way to initialize the editor context from
+ // AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has
+ // a TextViewer, not a TextEditor, instance).
+ //
+ // Therefore, for now, use a hack. This hack is reasonable because hyperlink
+ // resolvers are only run for the front-most visible window in the active
+ // workbench.
+ return AdtUtils.getActiveEditor();
+ }
+
+ /** Returns the project applicable to this hyperlink detection */
+ @Nullable
+ private static IProject getProject() {
+ IFile file = AdtUtils.getActiveFile();
+ if (file != null) {
+ return file.getProject();
+ }
+
+ return null;
+ }
+
+ /**
+ * Hyperlink implementation which delays computing the actual file and offset target
+ * until it is asked to open the hyperlink
+ */
+ private static class DeferredResolutionLink implements IHyperlink {
+ private XmlContext mXmlContext;
+ private IRegion mRegion;
+
+ public DeferredResolutionLink(XmlContext xmlContext, IRegion mRegion) {
+ super();
+ this.mXmlContext = xmlContext;
+ this.mRegion = mRegion;
+ }
+
+ @Override
+ public IRegion getHyperlinkRegion() {
+ return mRegion;
+ }
+
+ @Override
+ public String getHyperlinkText() {
+ return "Open XML Declaration";
+ }
+
+ @Override
+ public String getTypeLabel() {
+ return null;
+ }
+
+ @Override
+ public void open() {
+ // Lazily compute the location to open
+ if (mXmlContext != null && !Hyperlinks.open(mXmlContext)) {
+ // Failed: display message to the user
+ displayError("Could not open link");
+ }
+ }
+ }
+
+ /**
+ * Hyperlink implementation which provides a link for a resource; the actual file name
+ * is known, but the value location within XML files is deferred until the link is
+ * actually opened.
+ */
+ static class ResourceLink implements IHyperlink {
+ private final String mLinkText;
+ private final IRegion mLinkRegion;
+ private final ResourceType mType;
+ private final String mName;
+ private final ResourceFile mFile;
+
+ /**
+ * Constructs a new {@link ResourceLink}.
+ *
+ * @param linkText the description of the link to be shown in a popup when there
+ * is more than one match
+ * @param linkRegion the region corresponding to the link source highlight
+ * @param file the target resource file containing the link definition
+ * @param type the type of resource being linked to
+ * @param name the name of the resource being linked to
+ */
+ public ResourceLink(String linkText, IRegion linkRegion, ResourceFile file,
+ ResourceType type, String name) {
+ super();
+ mLinkText = linkText;
+ mLinkRegion = linkRegion;
+ mType = type;
+ mName = name;
+ mFile = file;
+ }
+
+ @Override
+ public IRegion getHyperlinkRegion() {
+ return mLinkRegion;
+ }
+
+ @Override
+ public String getHyperlinkText() {
+ // return "Open XML Declaration";
+ return mLinkText;
+ }
+
+ @Override
+ public String getTypeLabel() {
+ return null;
+ }
+
+ @Override
+ public void open() {
+ // We have to defer computation of ids until the link is clicked since we
+ // don't have a fast map lookup for these
+ if (mFile == null && mType == ResourceType.ID) {
+ // Id's are handled specially because they are typically defined
+ // inline (though they -can- be defined in the values folder above as well,
+ // in which case we will prefer that definition)
+ IProject project = getProject();
+ Pair<IFile,IRegion> def = findIdDefinition(project, mName);
+ if (def != null) {
+ try {
+ AdtPlugin.openFile(def.getFirst(), def.getSecond());
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ return;
+ }
+
+ displayError(String.format("Could not find id %1$s", mName));
+ return;
+ }
+
+ IAbstractFile wrappedFile = mFile != null ? mFile.getFile() : null;
+ if (wrappedFile instanceof IFileWrapper) {
+ IFile file = ((IFileWrapper) wrappedFile).getIFile();
+ try {
+ // Lazily search for the target?
+ IRegion region = null;
+ String extension = file.getFileExtension();
+ if (mType != null && mName != null && EXT_XML.equals(extension)) {
+ Pair<IFile, IRegion> target;
+ if (mType == ResourceType.ID) {
+ target = findIdInXml(mName, file);
+ } else {
+ target = findValueInXml(mType, mName, file);
+ }
+ if (target != null) {
+ region = target.getSecond();
+ }
+ }
+ AdtPlugin.openFile(file, region);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ } else if (wrappedFile instanceof FileWrapper) {
+ File file = ((FileWrapper) wrappedFile);
+ IPath path = new Path(file.getAbsolutePath());
+ int offset = 0;
+ // Lazily search for the target?
+ if (mType != null && mName != null && EXT_XML.equals(path.getFileExtension())) {
+ if (file.exists()) {
+ Pair<File, Integer> target = findValueInXml(mType, mName, file);
+ if (target != null && target.getSecond() != null) {
+ offset = target.getSecond();
+ }
+ }
+ }
+ openPath(path, null, offset);
+ } else {
+ throw new IllegalArgumentException("Invalid link parameters");
+ }
+ }
+
+ ResourceFile getFile() {
+ return mFile;
+ }
+ }
+
+ /**
+ * XML context containing node, potentially attribute, and text regions surrounding a
+ * particular caret offset
+ */
+ private static class XmlContext {
+ private final Node mNode;
+ private final Element mElement;
+ private final Attr mAttribute;
+ private final IStructuredDocumentRegion mOuterRegion;
+ private final ITextRegion mInnerRegion;
+ private final int mInnerRegionOffset;
+
+ public XmlContext(Node node, Element element, Attr attribute,
+ IStructuredDocumentRegion outerRegion,
+ ITextRegion innerRegion, int innerRegionOffset) {
+ super();
+ mNode = node;
+ mElement = element;
+ mAttribute = attribute;
+ mOuterRegion = outerRegion;
+ mInnerRegion = innerRegion;
+ mInnerRegionOffset = innerRegionOffset;
+ }
+
+ /**
+ * Gets the current node, never null
+ *
+ * @return the surrounding node
+ */
+ public Node getNode() {
+ return mNode;
+ }
+
+
+ /**
+ * Gets the current node, may be null
+ *
+ * @return the surrounding node
+ */
+ public Element getElement() {
+ return mElement;
+ }
+
+ /**
+ * Returns the current attribute, or null if we are not over an attribute
+ *
+ * @return the attribute, or null
+ */
+ public Attr getAttribute() {
+ return mAttribute;
+ }
+
+ /**
+ * Gets the region of the element
+ *
+ * @return the region of the surrounding element, never null
+ */
+ public ITextRegion getElementRegion() {
+ return mOuterRegion;
+ }
+
+ /**
+ * Gets the inner region, which can be the tag name, an attribute name, an
+ * attribute value, or some other portion of an XML element
+ * @return the inner region, never null
+ */
+ public ITextRegion getInnerRegion() {
+ return mInnerRegion;
+ }
+
+ /**
+ * Gets the caret offset relative to the inner region
+ *
+ * @return the offset relative to the inner region
+ */
+ public int getInnerRegionCaretOffset() {
+ return mInnerRegionOffset;
+ }
+
+ /**
+ * Returns a range with suffix whitespace stripped out
+ *
+ * @param document the document containing the regions
+ * @return the range of the inner region, minus any whitespace at the end
+ */
+ public IRegion getInnerRange(IDocument document) {
+ int start = mOuterRegion.getStart() + mInnerRegion.getStart();
+ int length = mInnerRegion.getLength();
+ try {
+ String s = document.get(start, length);
+ for (int i = s.length() - 1; i >= 0; i--) {
+ if (Character.isWhitespace(s.charAt(i))) {
+ length--;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, ""); //$NON-NLS-1$
+ }
+ return new Region(start, length);
+ }
+
+ /**
+ * Returns the node the cursor is currently on in the document. null if no node is
+ * selected
+ */
+ private static XmlContext find(IDocument document, int offset) {
+ // Loosely based on getCurrentNode and getCurrentAttr in the WST's
+ // XMLHyperlinkDetector.
+ IndexedRegion inode = null;
+ IStructuredModel model = null;
+ try {
+ model = StructuredModelManager.getModelManager().getExistingModelForRead(document);
+ if (model != null) {
+ inode = model.getIndexedRegion(offset);
+ if (inode == null) {
+ inode = model.getIndexedRegion(offset - 1);
+ }
+
+ if (inode instanceof Element) {
+ Element element = (Element) inode;
+ Attr attribute = null;
+ if (element.hasAttributes()) {
+ NamedNodeMap attrs = element.getAttributes();
+ // go through each attribute in node and if attribute contains
+ // offset, return that attribute
+ for (int i = 0; i < attrs.getLength(); ++i) {
+ // assumption that if parent node is of type IndexedRegion,
+ // then its attributes will also be of type IndexedRegion
+ IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
+ if (attRegion.contains(offset)) {
+ attribute = (Attr) attrs.item(i);
+ break;
+ }
+ }
+ }
+
+ IStructuredDocument doc = model.getStructuredDocument();
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ if (region != null
+ && DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
+ ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
+ if (subRegion == null) {
+ return null;
+ }
+ int regionStart = region.getStartOffset();
+ int subregionStart = subRegion.getStart();
+ int relativeOffset = offset - (regionStart + subregionStart);
+ return new XmlContext(element, element, attribute, region, subRegion,
+ relativeOffset);
+ }
+ } else if (inode instanceof Node) {
+ IStructuredDocument doc = model.getStructuredDocument();
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ if (region != null
+ && DOMRegionContext.XML_CONTENT.equals(region.getType())) {
+ ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
+ int regionStart = region.getStartOffset();
+ int subregionStart = subRegion.getStart();
+ int relativeOffset = offset - (regionStart + subregionStart);
+ return new XmlContext((Node) inode, null, null, region, subRegion,
+ relativeOffset);
+ }
+
+ }
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * DOM parser which records offsets in the element nodes such that it can return
+ * offsets for elements later
+ */
+ private static final class OffsetTrackingParser extends DOMParser {
+
+ private static final String KEY_OFFSET = "offset"; //$NON-NLS-1$
+
+ private static final String KEY_NODE =
+ "http://apache.org/xml/properties/dom/current-element-node"; //$NON-NLS-1$
+
+ private XMLLocator mLocator;
+
+ public OffsetTrackingParser() throws SAXException {
+ this.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",//$NON-NLS-1$
+ false);
+ }
+
+ public int getOffset(Node node) {
+ Integer offset = (Integer) node.getUserData(KEY_OFFSET);
+ if (offset != null) {
+ return offset;
+ }
+
+ return -1;
+ }
+
+ @Override
+ public void startElement(QName elementQName, XMLAttributes attrList, Augmentations augs)
+ throws XNIException {
+ int offset = mLocator.getCharacterOffset();
+ super.startElement(elementQName, attrList, augs);
+
+ try {
+ Node node = (Node) this.getProperty(KEY_NODE);
+ if (node != null) {
+ node.setUserData(KEY_OFFSET, offset, null);
+ }
+ } catch (org.xml.sax.SAXException ex) {
+ AdtPlugin.log(ex, ""); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public void startDocument(XMLLocator locator, String encoding,
+ NamespaceContext namespaceContext, Augmentations augs) throws XNIException {
+ super.startDocument(locator, encoding, namespaceContext, augs);
+ mLocator = locator;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IPageImageProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IPageImageProvider.java
new file mode 100755
index 000000000..7cd80ec1d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IPageImageProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Interface that editor pages can implement to provide an icon
+ * for the page tab in the XML editor.
+ */
+public interface IPageImageProvider {
+
+ /**
+ * Returns an {@link Image} that the editor will display in the page's tab.
+ *
+ * @return An {@link Image} for the editor tab for this page. Null for no image.
+ */
+ Image getPageImage();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java
new file mode 100644
index 000000000..41807f82b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite;
+import com.google.common.collect.Maps;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+
+import java.net.URL;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * Factory to generate icons for Android Editors.
+ * <p/>
+ * Icons are kept here and reused.
+ */
+public class IconFactory {
+ public static final int COLOR_RED = SWT.COLOR_DARK_RED;
+ public static final int COLOR_GREEN = SWT.COLOR_DARK_GREEN;
+ public static final int COLOR_BLUE = SWT.COLOR_DARK_BLUE;
+ public static final int COLOR_DEFAULT = SWT.COLOR_BLACK;
+
+ public static final int SHAPE_CIRCLE = 'C';
+ public static final int SHAPE_RECT = 'R';
+ public static final int SHAPE_DEFAULT = SHAPE_CIRCLE;
+
+ private static IconFactory sInstance;
+
+ private Map<String, Image> mIconMap = Maps.newHashMap();
+ private Map<URL, Image> mUrlMap = Maps.newHashMap();
+ private Map<String, ImageDescriptor> mImageDescMap = Maps.newHashMap();
+ private Map<Image, Image> mErrorIcons;
+ private Map<Image, Image> mWarningIcons;
+
+ private IconFactory() {
+ }
+
+ public static synchronized IconFactory getInstance() {
+ if (sInstance == null) {
+ sInstance = new IconFactory();
+ }
+ return sInstance;
+ }
+
+ public void dispose() {
+ // Dispose icons
+ for (Image icon : mIconMap.values()) {
+ // The map can contain null values
+ if (icon != null) {
+ icon.dispose();
+ }
+ }
+ mIconMap.clear();
+ for (Image icon : mUrlMap.values()) {
+ // The map can contain null values
+ if (icon != null) {
+ icon.dispose();
+ }
+ }
+ mUrlMap.clear();
+ if (mErrorIcons != null) {
+ for (Image icon : mErrorIcons.values()) {
+ // The map can contain null values
+ if (icon != null) {
+ icon.dispose();
+ }
+ }
+ mErrorIcons = null;
+ }
+ if (mWarningIcons != null) {
+ for (Image icon : mWarningIcons.values()) {
+ // The map can contain null values
+ if (icon != null) {
+ icon.dispose();
+ }
+ }
+ mWarningIcons = null;
+ }
+ }
+
+ /**
+ * Returns an Image for a given icon name.
+ * <p/>
+ * Callers should not dispose it.
+ *
+ * @param osName The leaf name, without the extension, of an existing icon in the
+ * editor's "icons" directory. If it doesn't exists, a default icon will be
+ * generated automatically based on the name.
+ */
+ public Image getIcon(String osName) {
+ return getIcon(osName, COLOR_DEFAULT, SHAPE_DEFAULT);
+ }
+
+ /**
+ * Returns an Image for a given icon name.
+ * <p/>
+ * Callers should not dispose it.
+ *
+ * @param osName The leaf name, without the extension, of an existing icon in the
+ * editor's "icons" directory. If it doesn't exist, a default icon will be
+ * generated automatically based on the name.
+ * @param color The color of the text in the automatically generated icons,
+ * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED.
+ * @param shape The shape of the icon in the automatically generated icons,
+ * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT.
+ */
+ public Image getIcon(String osName, int color, int shape) {
+ String key = Character.toString((char) shape) + Integer.toString(color) + osName;
+ Image icon = mIconMap.get(key);
+ if (icon == null && !mIconMap.containsKey(key)) {
+ ImageDescriptor id = getImageDescriptor(osName, color, shape);
+ if (id != null) {
+ icon = id.createImage();
+ }
+ // Note that we store null references in the icon map, to avoid looking them
+ // up every time. If it didn't exist once, it will not exist later.
+ mIconMap.put(key, icon);
+ }
+ return icon;
+ }
+
+ /**
+ * Returns an ImageDescriptor for a given icon name.
+ * <p/>
+ * Callers should not dispose it.
+ *
+ * @param osName The leaf name, without the extension, of an existing icon in the
+ * editor's "icons" directory. If it doesn't exists, a default icon will be
+ * generated automatically based on the name.
+ */
+ public ImageDescriptor getImageDescriptor(String osName) {
+ return getImageDescriptor(osName, COLOR_DEFAULT, SHAPE_DEFAULT);
+ }
+
+ /**
+ * Returns an ImageDescriptor for a given icon name.
+ * <p/>
+ * Callers should not dispose it.
+ *
+ * @param osName The leaf name, without the extension, of an existing icon in the
+ * editor's "icons" directory. If it doesn't exists, a default icon will be
+ * generated automatically based on the name.
+ * @param color The color of the text in the automatically generated icons.
+ * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED.
+ * @param shape The shape of the icon in the automatically generated icons,
+ * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT.
+ */
+ public ImageDescriptor getImageDescriptor(String osName, int color, int shape) {
+ String key = Character.toString((char) shape) + Integer.toString(color) + osName;
+ ImageDescriptor id = mImageDescMap.get(key);
+ if (id == null && !mImageDescMap.containsKey(key)) {
+ id = AbstractUIPlugin.imageDescriptorFromPlugin(
+ AdtPlugin.PLUGIN_ID,
+ String.format("/icons/%1$s.png", osName)); //$NON-NLS-1$
+
+ if (id == null) {
+ id = new LetterImageDescriptor(osName.charAt(0), color, shape);
+ }
+
+ // Note that we store null references in the icon map, to avoid looking them
+ // up every time. If it didn't exist once, it will not exist later.
+ mImageDescMap.put(key, id);
+ }
+ return id;
+ }
+
+ /**
+ * Returns an Image for a given icon name.
+ * <p/>
+ * Callers should not dispose it.
+ *
+ * @param osName The leaf name, without the extension, of an existing icon
+ * in the editor's "icons" directory. If it doesn't exist, the
+ * fallback will be used instead.
+ * @param fallback the fallback icon name to use if the primary icon does
+ * not exist, or null if the method should return null if the
+ * image does not exist
+ * @return the icon, which should not be disposed by the caller, or null
+ * if the image does not exist *and*
+ */
+ @Nullable
+ public Image getIcon(@NonNull String osName, @Nullable String fallback) {
+ String key = osName;
+ Image icon = mIconMap.get(key);
+ if (icon == null && !mIconMap.containsKey(key)) {
+ ImageDescriptor id = getImageDescriptor(osName, fallback);
+ if (id != null) {
+ icon = id.createImage();
+ }
+ // Note that we store null references in the icon map, to avoid looking them
+ // up every time. If it didn't exist once, it will not exist later.
+ mIconMap.put(key, icon);
+ }
+ return icon;
+ }
+
+ /**
+ * Returns an icon of the given name, or if that image does not exist and
+ * icon of the given fallback name.
+ *
+ * @param key the icon name
+ * @param fallbackKey the fallback image to use if the primary key does not
+ * exist
+ * @return the image descriptor, or null if the image does not exist and the
+ * fallbackKey is null
+ */
+ @Nullable
+ public ImageDescriptor getImageDescriptor(@NonNull String key, @Nullable String fallbackKey) {
+ ImageDescriptor id = mImageDescMap.get(key);
+ if (id == null && !mImageDescMap.containsKey(key)) {
+ id = AbstractUIPlugin.imageDescriptorFromPlugin(
+ AdtPlugin.PLUGIN_ID,
+ String.format("/icons/%1$s.png", key)); //$NON-NLS-1$
+ if (id == null) {
+ if (fallbackKey == null) {
+ return null;
+ }
+ id = getImageDescriptor(fallbackKey);
+ }
+
+ // Place the fallback image for this key as well such that we don't keep trying
+ // to load the failed image
+ mImageDescMap.put(key, id);
+ }
+
+ return id;
+ }
+
+ /**
+ * Returns the image indicated by the given URL
+ *
+ * @param url the url to the image resources
+ * @return the image for the url, or null if it cannot be initialized
+ */
+ public Image getIcon(URL url) {
+ Image image = mUrlMap.get(url);
+ if (image == null) {
+ ImageDescriptor descriptor = ImageDescriptor.createFromURL(url);
+ image = descriptor.createImage();
+ mUrlMap.put(url, image);
+ }
+
+ return image;
+ }
+
+ /**
+ * Returns an image with an error icon overlaid on it. The icons are cached,
+ * so the base image should be cached as well, or this method will keep
+ * storing new overlays into its cache.
+ *
+ * @param image the base image
+ * @return the combined image
+ */
+ @NonNull
+ public Image addErrorIcon(@NonNull Image image) {
+ if (mErrorIcons != null) {
+ Image combined = mErrorIcons.get(image);
+ if (combined != null) {
+ return combined;
+ }
+ } else {
+ mErrorIcons = new IdentityHashMap<Image, Image>();
+ }
+
+ Image combined = new ErrorImageComposite(image, false).createImage();
+ mErrorIcons.put(image, combined);
+
+ return combined;
+ }
+
+ /**
+ * Returns an image with a warning icon overlaid on it. The icons are
+ * cached, so the base image should be cached as well, or this method will
+ * keep storing new overlays into its cache.
+ *
+ * @param image the base image
+ * @return the combined image
+ */
+ @NonNull
+ public Image addWarningIcon(@NonNull Image image) {
+ if (mWarningIcons != null) {
+ Image combined = mWarningIcons.get(image);
+ if (combined != null) {
+ return combined;
+ }
+ } else {
+ mWarningIcons = new IdentityHashMap<Image, Image>();
+ }
+
+ Image combined = new ErrorImageComposite(image, true).createImage();
+ mWarningIcons.put(image, combined);
+
+ return combined;
+ }
+
+ /**
+ * A simple image description that generates a 16x16 image which consists
+ * of a colored letter inside a black & white circle.
+ */
+ private static class LetterImageDescriptor extends ImageDescriptor {
+
+ private final char mLetter;
+ private final int mColor;
+ private final int mShape;
+
+ public LetterImageDescriptor(char letter, int color, int shape) {
+ mLetter = Character.toUpperCase(letter);
+ mColor = color;
+ mShape = shape;
+ }
+
+ @Override
+ public ImageData getImageData() {
+
+ final int SX = 15;
+ final int SY = 15;
+ final int RX = 4;
+ final int RY = 4;
+
+ Display display = Display.getCurrent();
+ if (display == null) {
+ return null;
+ }
+
+ Image image = new Image(display, SX, SY);
+
+ GC gc = new GC(image);
+ gc.setAdvanced(true);
+ gc.setAntialias(SWT.ON);
+ gc.setTextAntialias(SWT.ON);
+
+ // image.setBackground() does not appear to have any effect; we must explicitly
+ // paint into the image the background color we want masked out later.
+ // HOWEVER, alpha transparency does not work; we only get to mark a single color
+ // as transparent. You might think we could pick a system color (to avoid having
+ // to allocate and dispose the color), or a wildly unique color (to make sure we
+ // don't accidentally pick up any extra pixels in the image as transparent), but
+ // this has the very unfortunate side effect of making neighbor pixels in the
+ // antialiased rendering of the circle pick up shades of that alternate color,
+ // which looks bad. Therefore we pick a color which is similar to one of our
+ // existing colors but hopefully different from most pixels. A visual check
+ // confirms that this seems to work pretty well:
+ RGB backgroundRgb = new RGB(254, 254, 254);
+ Color backgroundColor = new Color(display, backgroundRgb);
+ gc.setBackground(backgroundColor);
+ gc.fillRectangle(0, 0, SX, SY);
+
+ gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
+ if (mShape == SHAPE_CIRCLE) {
+ gc.fillOval(0, 0, SX - 1, SY - 1);
+ } else if (mShape == SHAPE_RECT) {
+ gc.fillRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY);
+ }
+
+ gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
+ gc.setLineWidth(1);
+ if (mShape == SHAPE_CIRCLE) {
+ gc.drawOval(0, 0, SX - 1, SY - 1);
+ } else if (mShape == SHAPE_RECT) {
+ gc.drawRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY);
+ }
+
+ // Get a bold version of the default system font, if possible.
+ Font font = display.getSystemFont();
+ FontData[] fds = font.getFontData();
+ fds[0].setStyle(SWT.BOLD);
+ // use 3/4th of the circle diameter for the font size (in pixels)
+ // and convert it to "font points" (font points in SWT are hardcoded in an
+ // arbitrary 72 dpi and then converted in real pixels using whatever is
+ // indicated by getDPI -- at least that's how it works under Win32).
+ fds[0].setHeight((int) ((SY + 1) * 3./4. * 72./display.getDPI().y));
+ // Note: win32 implementation always uses fds[0] so we change just that one.
+ // getFontData indicates that the array of fd is really an unusual thing for X11.
+ font = new Font(display, fds);
+ gc.setFont(font);
+ gc.setForeground(display.getSystemColor(mColor));
+
+ // Text measurement varies so slightly depending on the platform
+ int ofx = 0;
+ int ofy = 0;
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
+ ofx = +1;
+ ofy = -1;
+ } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ // Tweak pixel positioning of some letters that don't look good on the Mac
+ if (mLetter != 'T' && mLetter != 'V') {
+ ofy = -1;
+ }
+ if (mLetter == 'I') {
+ ofx = -2;
+ }
+ }
+
+ String s = Character.toString(mLetter);
+ Point p = gc.textExtent(s);
+ int tx = (SX + ofx - p.x) / 2;
+ int ty = (SY + ofy - p.y) / 2;
+ gc.drawText(s, tx, ty, true /* isTransparent */);
+
+ font.dispose();
+ gc.dispose();
+
+ ImageData data = image.getImageData();
+ image.dispose();
+ backgroundColor.dispose();
+
+ // Set transparent pixel in the palette such that on paint (over palette,
+ // which has a background of SWT.COLOR_WIDGET_BACKGROUND, and over the tree
+ // which has a white background) we will substitute the background in for
+ // the backgroundPixel.
+ int backgroundPixel = data.palette.getPixel(backgroundRgb);
+ data.transparentPixel = backgroundPixel;
+
+ return data;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java
new file mode 100644
index 000000000..bb5d1ba01
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_SRC;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.DRAWABLE_PREFIX;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.VIEW;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.wst.xml.ui.internal.contentoutline.JFaceNodeLabelProvider;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Element;
+
+/**
+ * Label provider for the XML outlines and quick outlines: Use our own icons,
+ * when available, and and include the most important attribute (id, name, or
+ * text)
+ */
+@SuppressWarnings("restriction")
+// XML UI API
+class OutlineLabelProvider extends JFaceNodeLabelProvider {
+ @Override
+ public Image getImage(Object element) {
+ if (element instanceof Element) {
+ Element e = (Element) element;
+ String tagName = e.getTagName();
+ if (VIEW_TAG.equals(tagName)) {
+ // Can't have both view.png and View.png; issues on case sensitive vs
+ // case insensitive file systems
+ tagName = VIEW;
+ }
+ IconFactory factory = IconFactory.getInstance();
+ Image img = factory.getIcon(tagName, null);
+ if (img != null) {
+ return img;
+ }
+ }
+ return super.getImage(element);
+ }
+
+ @Override
+ public String getText(Object element) {
+ String text = super.getText(element);
+ if (element instanceof Element) {
+ Element e = (Element) element;
+ String id = getAttributeNS(e, ANDROID_URI, ATTR_ID);
+ if (id == null || id.length() == 0) {
+ id = getAttributeNS(e, ANDROID_URI, ATTR_NAME);
+ if (id == null || id.length() == 0) {
+ id = e.getAttribute(ATTR_NAME);
+ if (id == null || id.length() == 0) {
+ id = getAttributeNS(e, ANDROID_URI, ATTR_TEXT);
+ if (id != null && id.length() > 15) {
+ id = id.substring(0, 12) + "...";
+ }
+ if (id == null || id.length() == 0) {
+ id = getAttributeNS(e, ANDROID_URI, ATTR_SRC);
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(DRAWABLE_PREFIX)) {
+ id = id.substring(DRAWABLE_PREFIX.length());
+ }
+ } else {
+ id = e.getAttribute(ATTR_LAYOUT);
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ id = id.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if (id != null && id.length() > 0) {
+ return text + ": " + id; //$NON-NLS-1$
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Wrapper around {@link Element#getAttributeNS(String, String)}.
+ * <p/>
+ * The implementation used in Eclipse's XML editor sometimes internally
+ * throws an NPE instead of politely returning null.
+ *
+ * @see Element#getAttributeNS(String, String)
+ */
+ private String getAttributeNS(Element e, String uri, String name) throws DOMException {
+ try {
+ return e.getAttributeNS(uri, name);
+ } catch (NullPointerException ignore) {
+ return null;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/XmlEditorMultiOutline.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/XmlEditorMultiOutline.java
new file mode 100644
index 000000000..61db9f313
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/XmlEditorMultiOutline.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.IPageBookViewPage;
+import org.eclipse.ui.part.Page;
+import org.eclipse.ui.part.PageBook;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Outline used for XML editors that have multiple pages with separate outlines:
+ * switches between them
+ * <p>
+ * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
+ * <p>
+ * Modeled after .org.eclipse.pde.internal.ui.editor.PDEMultiPageContentOutline
+ */
+public class XmlEditorMultiOutline extends Page implements IContentOutlinePage,
+ ISelectionChangedListener {
+ private boolean mDisposed;
+ private PageBook mPageBook;
+ private IContentOutlinePage mCurrentPage;
+ private IActionBars mActionBars;
+ private IContentOutlinePage mEmptyPage;
+ private List<ISelectionChangedListener> mListeners;
+ private ISelection mSelection;
+
+ public XmlEditorMultiOutline() {
+ }
+
+ @Override
+ public Control getControl() {
+ return mPageBook;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mPageBook = new PageBook(parent, SWT.NONE);
+ }
+
+ @Override
+ public void dispose() {
+ mDisposed = true;
+ mListeners = null;
+ if (mPageBook != null && !mPageBook.isDisposed()) {
+ mPageBook.dispose();
+ mPageBook = null;
+ }
+ if (mEmptyPage != null) {
+ mEmptyPage.dispose();
+ mEmptyPage = null;
+ }
+ }
+
+ public boolean isDisposed() {
+ return mDisposed;
+ }
+
+ @Override
+ public void makeContributions(IMenuManager menuManager, IToolBarManager toolBarManager,
+ IStatusLineManager statusLineManager) {
+ }
+
+ @Override
+ public void setActionBars(IActionBars actionBars) {
+ mActionBars = actionBars;
+ if (mCurrentPage != null) {
+ setPageActive(mCurrentPage);
+ }
+ }
+
+ @Override
+ public void setFocus() {
+ if (mCurrentPage != null) {
+ mCurrentPage.setFocus();
+ }
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<ISelectionChangedListener>(2);
+ }
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public ISelection getSelection() {
+ return mSelection;
+ }
+
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ setSelection(event.getSelection());
+ }
+
+ public void setPageActive(IContentOutlinePage page) {
+ if (page == null) {
+ if (mEmptyPage == null) {
+ mEmptyPage = new EmptyPage();
+ }
+ page = mEmptyPage;
+ }
+ if (mCurrentPage != null) {
+ mCurrentPage.removeSelectionChangedListener(this);
+ }
+ page.addSelectionChangedListener(this);
+ mCurrentPage = page;
+ // Still initializing?
+ if (mPageBook == null) {
+ return;
+ }
+ Control control = page.getControl();
+ if (control == null || control.isDisposed()) {
+ if (page instanceof IPageBookViewPage) {
+ try {
+ ((IPageBookViewPage) page).init(getSite());
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ page.createControl(mPageBook);
+ page.setActionBars(mActionBars);
+ control = page.getControl();
+ }
+ mPageBook.showPage(control);
+ }
+
+ @Override
+ public void setSelection(ISelection selection) {
+ mSelection = selection;
+ if (mListeners != null) {
+ SelectionChangedEvent e = new SelectionChangedEvent(this, selection);
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).selectionChanged(e);
+ }
+ }
+ }
+
+ private static class EmptyPage implements IContentOutlinePage {
+ private Composite mControl;
+
+ private EmptyPage() {
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mControl = new Composite(parent, SWT.NULL);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public Control getControl() {
+ return mControl;
+ }
+
+ @Override
+ public void setActionBars(IActionBars actionBars) {
+ }
+
+ @Override
+ public void setFocus() {
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ }
+
+ @Override
+ public ISelection getSelection() {
+ return StructuredSelection.EMPTY;
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ }
+
+ @Override
+ public void setSelection(ISelection selection) {
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimDescriptors.java
new file mode 100644
index 000000000..2489cf57f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimDescriptors.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.animator;
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Descriptors for the res/anim resources */
+public class AnimDescriptors implements IDescriptorProvider {
+ /** The root element descriptor */
+ private ElementDescriptor mDescriptor;
+ /** The root element descriptors */
+ private ElementDescriptor[] mRootDescriptors;
+ private Map<String, ElementDescriptor> nameToDescriptor;
+
+ /** @return the root descriptor. */
+ @Override
+ public ElementDescriptor getDescriptor() {
+ if (mDescriptor == null) {
+ mDescriptor = new ElementDescriptor("", getRootElementDescriptors()); //$NON-NLS-1$
+ }
+
+ return mDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mRootDescriptors;
+ }
+
+ public ElementDescriptor getElementDescriptor(String mRootTag) {
+ if (nameToDescriptor == null) {
+ nameToDescriptor = new HashMap<String, ElementDescriptor>();
+ for (ElementDescriptor descriptor : getRootElementDescriptors()) {
+ nameToDescriptor.put(descriptor.getXmlName(), descriptor);
+ }
+ }
+
+ ElementDescriptor descriptor = nameToDescriptor.get(mRootTag);
+ if (descriptor == null) {
+ descriptor = getDescriptor();
+ }
+ return descriptor;
+ }
+
+ public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) {
+ if (styleMap == null) {
+ return;
+ }
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(SdkConstants.ANDROID_NS_NAME,
+ SdkConstants.ANDROID_URI);
+
+ List<ElementDescriptor> descriptors = new ArrayList<ElementDescriptor>();
+
+ String sdkUrl =
+ "http://developer.android.com/guide/topics/graphics/view-animation.html"; //$NON-NLS-1$
+ ElementDescriptor set = AnimatorDescriptors.addElement(descriptors, styleMap,
+ "set", "Set", "AnimationSet", "Animation", //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$
+ "A container that holds other animation elements (<alpha>, <scale>, "
+ + "<translate>, <rotate>) or other <set> elements. ",
+ sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "alpha", "Alpha", "AlphaAnimation", "Animation", //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$
+ "A fade-in or fade-out animation.",
+ sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "scale", "Scale", "ScaleAnimation", "Animation", //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$
+ "A resizing animation. You can specify the center point of the image from "
+ + "which it grows outward (or inward) by specifying pivotX and pivotY. "
+ + "For example, if these values are 0, 0 (top-left corner), all growth "
+ + "will be down and to the right.",
+ sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "rotate", "Rotate", "RotateAnimation", "Animation", //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$
+ "A rotation animation.",
+ sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "translate", "Translate", "TranslateAnimation", "Animation", //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$
+ "A vertical and/or horizontal motion. Supports the following attributes in "
+ + "any of the following three formats: values from -100 to 100 ending "
+ + "with \"%\", indicating a percentage relative to itself; values from "
+ + "-100 to 100 ending in \"%p\", indicating a percentage relative to its "
+ + "parent; a float value with no suffix, indicating an absolute value.",
+ sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ mRootDescriptors = descriptors.toArray(new ElementDescriptor[descriptors.size()]);
+
+ // Allow <set> to nest the others (and other sets)
+ if (set != null) {
+ set.setChildren(mRootDescriptors);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java
new file mode 100644
index 000000000..8a4cf2384
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.animator;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_PKG;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Content Assist Processor for /res/drawable XML files
+ */
+@VisibleForTesting
+public final class AnimationContentAssist extends AndroidContentAssist {
+ private static final String OBJECT_ANIMATOR = "objectAnimator"; //$NON-NLS-1$
+ private static final String PROPERTY_NAME = "propertyName"; //$NON-NLS-1$
+ private static final String INTERPOLATOR_PROPERTY_NAME = "interpolator"; //$NON-NLS-1$
+ private static final String INTERPOLATOR_NAME_SUFFIX = "_interpolator"; //$NON-NLS-1$
+
+ public AnimationContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_ANIMATOR);
+ }
+
+ @Override
+ protected int getRootDescriptorId() {
+ String folderName = AdtUtils.getParentFolderName(mEditor.getEditorInput());
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
+ if (folderType == ResourceFolderType.ANIM) {
+ return AndroidTargetData.DESCRIPTOR_ANIM;
+ } else {
+ return AndroidTargetData.DESCRIPTOR_ANIMATOR;
+ }
+ }
+
+ @Override
+ protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset,
+ String parentTagName, String attributeName, Node node, String wordPrefix,
+ boolean skipEndTag, int replaceLength) {
+
+ // Add value completion for the interpolator and propertyName attributes
+
+ if (attributeName.endsWith(INTERPOLATOR_PROPERTY_NAME)) {
+ if (!wordPrefix.startsWith("@android:anim/")) { //$NON-NLS-1$
+ // List all framework interpolators with full path first
+ AndroidTargetData data = mEditor.getTargetData();
+ ResourceRepository repository = data.getFrameworkResources();
+ List<String> interpolators = new ArrayList<String>();
+ String base = '@' + ANDROID_PKG + ':' + ResourceType.ANIM.getName() + '/';
+ for (ResourceItem item : repository.getResourceItemsOfType(ResourceType.ANIM)) {
+ String name = item.getName();
+ if (name.endsWith(INTERPOLATOR_NAME_SUFFIX)) {
+ interpolators.add(base + item.getName());
+ }
+ }
+ addMatchingProposals(proposals, interpolators.toArray(), offset, node, wordPrefix,
+ (char) 0 /* needTag */, true /* isAttribute */, false /* isNew */,
+ skipEndTag /* skipEndTag */, replaceLength);
+ }
+
+
+ return super.computeAttributeValues(proposals, offset, parentTagName, attributeName,
+ node, wordPrefix, skipEndTag, replaceLength);
+ } else if (parentTagName.equals(OBJECT_ANIMATOR)
+ && attributeName.endsWith(PROPERTY_NAME)) {
+
+ // Special case: the user is code completing inside
+ // <objectAnimator propertyName="^">
+ // In this case, offer ALL attribute names that make sense for animation
+ // (e.g. all numeric ones)
+
+ String attributePrefix = wordPrefix;
+ if (startsWith(attributePrefix, ANDROID_NS_NAME_PREFIX)) {
+ attributePrefix = attributePrefix.substring(ANDROID_NS_NAME_PREFIX.length());
+ }
+
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ IDescriptorProvider descriptorProvider =
+ data.getDescriptorProvider(AndroidTargetData.DESCRIPTOR_LAYOUT);
+ if (descriptorProvider != null) {
+ ElementDescriptor[] rootElementDescriptors =
+ descriptorProvider.getRootElementDescriptors();
+ Map<String, AttributeDescriptor> matches =
+ new HashMap<String, AttributeDescriptor>(180);
+ for (ElementDescriptor elementDesc : rootElementDescriptors) {
+ for (AttributeDescriptor desc : elementDesc.getAttributes()) {
+ if (desc instanceof SeparatorAttributeDescriptor) {
+ continue;
+ }
+ String name = desc.getXmlLocalName();
+ if (startsWith(name, attributePrefix)) {
+ EnumSet<Format> formats = desc.getAttributeInfo().getFormats();
+ if (formats.contains(Format.INTEGER)
+ || formats.contains(Format.FLOAT)) {
+ // TODO: Filter out some common properties
+ // that the user probably isn't trying to
+ // animate:
+ // num*, min*, max*, *Index, *Threshold,
+ // *Duration, *Id, *Limit
+ matches.put(name, desc);
+ }
+ }
+ }
+ }
+
+ List<AttributeDescriptor> sorted =
+ new ArrayList<AttributeDescriptor>(matches.size());
+ sorted.addAll(matches.values());
+ Collections.sort(sorted);
+ // Extract just the name+description pairs, since we don't want to
+ // use the full attribute descriptor (which forces the namespace
+ // prefix to be included)
+ List<Pair<String, String>> pairs =
+ new ArrayList<Pair<String, String>>(sorted.size());
+ for (AttributeDescriptor d : sorted) {
+ pairs.add(Pair.of(d.getXmlLocalName(), d.getAttributeInfo().getJavaDoc()));
+ }
+
+ addMatchingProposals(proposals, pairs.toArray(), offset, node, wordPrefix,
+ (char) 0 /* needTag */, true /* isAttribute */, false /* isNew */,
+ skipEndTag /* skipEndTag */, replaceLength);
+ }
+ }
+
+ return false;
+ } else {
+ return super.computeAttributeValues(proposals, offset, parentTagName, attributeName,
+ node, wordPrefix, skipEndTag, replaceLength);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationEditorDelegate.java
new file mode 100644
index 000000000..7c7051de7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationEditorDelegate.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.animator;
+
+import static com.android.ide.eclipse.adt.AdtConstants.EDITORS_NAMESPACE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Editor for /res/animator XML files.
+ */
+@SuppressWarnings("restriction")
+public class AnimationEditorDelegate extends CommonXmlDelegate {
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public AnimationEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.ANIM == type || ResourceFolderType.ANIMATOR == type) {
+ return new AnimationEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ EDITORS_NAMESPACE + ".animator.AnimationEditor"; //$NON-NLS-1$
+
+ /** The tag used at the root */
+ private String mRootTag;
+
+ private AnimationEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new AnimationContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ @Override
+ public void delegateCreateFormPages() {
+ /* Disabled for now; doesn't work quite right
+ try {
+ addPage(new AnimatorTreePage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(IStatus.ERROR, "Error creating nested page"); //$NON-NLS-1$
+ AdtPlugin.getDefault().getLog().log(e.getStatus());
+ }
+ */
+ }
+
+ /**
+ * Processes the new XML Model.
+ *
+ * @param xmlDoc The XML document, if available, or null if none exists.
+ */
+ @Override
+ public void delegateXmlModelChanged(Document xmlDoc) {
+ Element rootElement = xmlDoc.getDocumentElement();
+ if (rootElement != null) {
+ mRootTag = rootElement.getTagName();
+ }
+
+ // create the ui root node on demand.
+ delegateInitUiRootNode(false /*force*/);
+
+ if (mRootTag != null
+ && !mRootTag.equals(getUiRootNode().getDescriptor().getXmlLocalName())) {
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data != null) {
+ ElementDescriptor descriptor;
+ if (getFolderType() == ResourceFolderType.ANIM) {
+ descriptor = data.getAnimDescriptors().getElementDescriptor(mRootTag);
+ } else {
+ descriptor = data.getAnimatorDescriptors().getElementDescriptor(mRootTag);
+ }
+ // Replace top level node now that we know the actual type
+
+ // Disconnect from old
+ getUiRootNode().setEditor(null);
+ getUiRootNode().setXmlDocument(null);
+
+ // Create new
+ setUiRootNode(descriptor.createUiNode());
+ getUiRootNode().setXmlDocument(xmlDoc);
+ getUiRootNode().setEditor(getEditor());
+ }
+ }
+
+ if (getUiRootNode().getDescriptor() instanceof DocumentDescriptor) {
+ getUiRootNode().loadFromXmlNode(xmlDoc);
+ } else {
+ getUiRootNode().loadFromXmlNode(rootElement);
+ }
+ }
+
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (getUiRootNode() == null || force) {
+ ElementDescriptor descriptor;
+ boolean reload = false;
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data == null) {
+ descriptor = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ if (getFolderType() == ResourceFolderType.ANIM) {
+ descriptor = data.getAnimDescriptors().getElementDescriptor(mRootTag);
+ } else {
+ descriptor = data.getAnimatorDescriptors().getElementDescriptor(mRootTag);
+ }
+ reload = true;
+ }
+ setUiRootNode(descriptor.createUiNode());
+ getUiRootNode().setEditor(getEditor());
+
+ if (reload) {
+ onDescriptorsChanged();
+ }
+ }
+ }
+
+ private ResourceFolderType getFolderType() {
+ String folderName = AdtUtils.getParentFolderName(getEditor().getEditorInput());
+ if (folderName.length() > 0) {
+ return ResourceFolderType.getFolderType(folderName);
+ }
+ return ResourceFolderType.ANIMATOR;
+ }
+
+ private void onDescriptorsChanged() {
+ IStructuredModel model = getEditor().getModelForRead();
+ if (model != null) {
+ try {
+ Node node = getEditor().getXmlDocument(model).getDocumentElement();
+ getUiRootNode().reloadFromXmlNode(node);
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimatorDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimatorDescriptors.java
new file mode 100644
index 000000000..713f6d92e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimatorDescriptors.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.animator;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Descriptors for /res/animator XML files.
+ */
+public class AnimatorDescriptors implements IDescriptorProvider {
+ /** The root element descriptor */
+ private ElementDescriptor mDescriptor;
+ /** The root element descriptors */
+ private ElementDescriptor[] mRootDescriptors;
+ private Map<String, ElementDescriptor> nameToDescriptor;
+
+ /** @return the root descriptor. */
+ @Override
+ public ElementDescriptor getDescriptor() {
+ if (mDescriptor == null) {
+ mDescriptor = new ElementDescriptor("", getRootElementDescriptors()); //$NON-NLS-1$
+ }
+
+ return mDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mRootDescriptors;
+ }
+
+ ElementDescriptor getElementDescriptor(String rootTag) {
+ if (nameToDescriptor == null) {
+ nameToDescriptor = new HashMap<String, ElementDescriptor>();
+ for (ElementDescriptor descriptor : getRootElementDescriptors()) {
+ nameToDescriptor.put(descriptor.getXmlName(), descriptor);
+ }
+ }
+
+ ElementDescriptor descriptor = nameToDescriptor.get(rootTag);
+ if (descriptor == null) {
+ descriptor = getDescriptor();
+ }
+ return descriptor;
+ }
+
+ public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) {
+ if (styleMap == null) {
+ return;
+ }
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(ANDROID_NS_NAME,
+ ANDROID_URI);
+
+ List<ElementDescriptor> descriptors = new ArrayList<ElementDescriptor>();
+
+ String sdkUrl =
+ "http://developer.android.com/guide/topics/graphics/animation.html"; //$NON-NLS-1$
+
+ ElementDescriptor set = addElement(descriptors, styleMap,
+ "set", "Animator Set", "AnimatorSet", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ ElementDescriptor objectAnimator = addElement(descriptors, styleMap,
+ "objectAnimator", "Object Animator", //$NON-NLS-1$
+ "PropertyAnimator", "Animator", //$NON-NLS-1$ //$NON-NLS-2$
+ null /* tooltip */, sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ ElementDescriptor animator = addElement(descriptors, styleMap,
+ "animator", "Animator", "Animator", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, sdkUrl,
+ xmlns, null, true /*mandatory*/);
+
+ mRootDescriptors = descriptors.toArray(new ElementDescriptor[descriptors.size()]);
+
+ // Allow arbitrary nesting: the children of all of these element can include
+ // any of the others
+ if (objectAnimator != null) {
+ objectAnimator.setChildren(mRootDescriptors);
+ }
+ if (animator != null) {
+ animator.setChildren(mRootDescriptors);
+ }
+ if (set != null) {
+ set.setChildren(mRootDescriptors);
+ }
+ }
+
+ /**
+ * Looks up the given style, and if found creates a new {@link ElementDescriptor}
+ * corresponding to the style. It can optionally take an extra style to merge in
+ * additional attributes from, and an extra attribute to add in as well. The new
+ * element, if it exists, can also be optionally added into a list.
+ *
+ * @param descriptors an optional list to add the element into, or null
+ * @param styleMap The map style => attributes from the attrs.xml file
+ * @param xmlName the XML tag name to use for the element
+ * @param uiName the UI name to display the element as
+ * @param styleName the name of the style which must exist for this style
+ * @param extraStyle an optional extra style to merge in attributes from, or null
+ * @param tooltip the tooltip or documentation for this element, or null
+ * @param sdkUrl an optional SDK url to display for the element, or null
+ * @param extraAttribute an extra attribute to add to the attributes list, or null
+ * @param childrenElements an array of children allowed by this element, or null
+ * @param mandatory if true, this element is mandatory
+ * @return a newly created element, or null if the style does not exist
+ */
+ public static ElementDescriptor addElement(
+ List<ElementDescriptor> descriptors,
+ Map<String, DeclareStyleableInfo> styleMap,
+ String xmlName, String uiName, String styleName, String extraStyle,
+ String tooltip, String sdkUrl,
+ AttributeDescriptor extraAttribute,
+ ElementDescriptor[] childrenElements,
+ boolean mandatory) {
+ DeclareStyleableInfo style = styleMap.get(styleName);
+ if (style == null) {
+ return null;
+ }
+ ElementDescriptor element = new ElementDescriptor(xmlName, uiName, tooltip, sdkUrl,
+ null, childrenElements, mandatory);
+
+ ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>();
+
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ ANDROID_URI,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ element.setTooltip(style.getJavaDoc());
+
+ if (extraStyle != null) {
+ style = styleMap.get(extraStyle);
+ if (style != null) {
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ ANDROID_URI,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ }
+ }
+
+ if (extraAttribute != null) {
+ descs.add(extraAttribute);
+ }
+
+ element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()]));
+ if (descriptors != null) {
+ descriptors.add(element);
+ }
+
+ return element;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLDescriber.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLDescriber.java
new file mode 100644
index 000000000..00bf7b0ac
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLDescriber.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.binaryxml;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.content.IContentDescriber;
+import org.eclipse.core.runtime.content.IContentDescription;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A content describer for Android binary xml files
+ *
+ * <p>
+ * This class referenced by the "describer" configuration element in
+ * extensions to the <code>org.eclipse.core.runtime.contentTypes</code>
+ * extension point.
+ * </p>
+ *
+ * References :
+ * <a>http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;
+ * f=include/utils/ResourceTypes.h</a>
+ *
+ */
+public class BinaryXMLDescriber implements IContentDescriber {
+
+ private static final int RES_XML_HEADER_SIZE = 8;
+ private final static short RES_XML_TYPE = 0x0003;
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.core.runtime.content.IContentDescriber#describe(java.io.
+ * InputStream, org.eclipse.core.runtime.content.IContentDescription)
+ */
+ @Override
+ public int describe(InputStream contents, IContentDescription description) throws IOException {
+ int status = INVALID;
+ int length = 8;
+ byte[] bytes = new byte[length];
+ if (contents.read(bytes, 0, length) == length) {
+ ByteBuffer buf = ByteBuffer.wrap(bytes);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ short type = buf.getShort();
+ short headerSize = buf.getShort();
+ int size = buf.getInt(); // chunk size
+ if (type == RES_XML_TYPE && headerSize == RES_XML_HEADER_SIZE) {
+ status = VALID;
+ }
+ }
+ return status;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.eclipse.core.runtime.content.IContentDescriber#getSupportedOptions()
+ */
+ @Override
+ public QualifiedName[] getSupportedOptions() {
+ return new QualifiedName[0];
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLMultiPageEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLMultiPageEditorPart.java
new file mode 100644
index 000000000..ae6a35d08
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/BinaryXMLMultiPageEditorPart.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.binaryxml;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.internal.core.JarEntryFile;
+import org.eclipse.jdt.internal.ui.javaeditor.JarEntryEditorInput;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.wst.xml.ui.internal.tabletree.XMLMultiPageEditorPart;
+
+import java.io.File;
+
+/**
+ * The XML editor is an editor that open Android xml files from the android.jar file
+ * <p>
+ * The editor checks if the file is contained in jar and is so,
+ * convert editor input to XmlStorageEditorInput that handles
+ * corresponding file from Android SDK.
+ *
+ */
+public class BinaryXMLMultiPageEditorPart extends XMLMultiPageEditorPart {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.part.EditorPart#setInput(org.eclipse.ui.IEditorInput)
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ if (input instanceof JarEntryEditorInput) {
+ JarEntryEditorInput jarInput = (JarEntryEditorInput) input;
+ IStorage storage = jarInput.getStorage();
+ if (storage instanceof JarEntryFile) {
+ JarEntryFile jarEntryFile = (JarEntryFile) storage;
+ IPackageFragmentRoot fragmentRoot = jarEntryFile.getPackageFragmentRoot();
+ if (fragmentRoot == null) {
+ super.setInput(input);
+ return;
+ }
+ IPath path = fragmentRoot.getPath();
+ if (path == null) {
+ super.setInput(input);
+ return;
+ }
+ path = path.removeLastSegments(1);
+ IPath filePath = path.append(SdkConstants.FD_DATA).append(
+ jarEntryFile.getFullPath().toPortableString());
+ File file = new File(filePath.toOSString());
+ if (!(file.isFile())) {
+ super.setInput(input);
+ return;
+ }
+ try {
+ XmlStorageEditorInput newInput = new XmlStorageEditorInput(
+ new FileStorage(file));
+ super.setInput(newInput);
+ return;
+ } catch (Exception e) {
+ AdtPlugin.log(e, e.getMessage(), null);
+ }
+ }
+ }
+ super.setInput(input);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/FileStorage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/FileStorage.java
new file mode 100644
index 000000000..a8c918283
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/FileStorage.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.binaryxml;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+/**
+ * Implementation of storage for a local file
+ * (<code>java.io.File</code>).
+ *
+ * @see org.eclipse.core.resources.IStorage
+ */
+
+public class FileStorage implements IStorage {
+
+ /**
+ * The file this storage refers to.
+ */
+ private File mFile = null;
+
+ /**
+ * Constructs and returns storage for the given file.
+ *
+ * @param file a local file
+ */
+ public FileStorage(File file) {
+ mFile = file;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.resources.IStorage#getContents()
+ */
+ @Override
+ public InputStream getContents() throws CoreException {
+ InputStream stream = null;
+ try {
+ stream = new FileInputStream(mFile);
+ } catch (Exception e) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.getDefault().getBundle()
+ .getSymbolicName(), IStatus.ERROR, mFile.getAbsolutePath(), e));
+ }
+ return stream;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.resources.IStorage#getFullPath()
+ */
+ @Override
+ public IPath getFullPath() {
+ return new Path(mFile.getAbsolutePath());
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.resources.IStorage#getName()
+ */
+ @Override
+ public String getName() {
+ return mFile.getName();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.resources.IStorage#isReadOnly()
+ */
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.runtime.IAdaptable#getAdapter(Class)
+ */
+ @Override
+ public Object getAdapter(Class adapter) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FileStorage) {
+ return mFile.equals(((FileStorage) obj).mFile);
+ }
+ return super.equals(obj);
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return mFile.hashCode();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/XmlStorageEditorInput.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/XmlStorageEditorInput.java
new file mode 100644
index 000000000..646c67587
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/binaryxml/XmlStorageEditorInput.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.binaryxml;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IPersistableElement;
+import org.eclipse.ui.IStorageEditorInput;
+
+/**
+ * An editor input for a local file.
+ */
+public class XmlStorageEditorInput implements IStorageEditorInput {
+
+ /**
+ * Storage associated with this editor input
+ */
+ IStorage mStorage = null;
+
+ /**
+ * Constructs an editor input on the given storage
+ *
+ * @param storage
+ */
+ public XmlStorageEditorInput(IStorage storage) {
+ mStorage = storage;
+ }
+
+ /* (non-Javadoc)
+ * @see IStorageEditorInput#getStorage()
+ */
+ @Override
+ public IStorage getStorage() throws CoreException {
+ return mStorage;
+ }
+
+ /* (non-Javadoc)
+ * @see IInput#getStorage()
+ */
+ @Override
+ public boolean exists() {
+ return mStorage != null;
+ }
+
+ /* (non-Javadoc)
+ * @see IEditorInput#getImageDescriptor()
+ */
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see IEditorInput#getName()
+ */
+ @Override
+ public String getName() {
+ return mStorage.getName();
+ }
+
+ /* (non-Javadoc)
+ * @see IEditorInput#getPersistable()
+ */
+ @Override
+ public IPersistableElement getPersistable() {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see IEditorInput#getToolTipText()
+ */
+ @Override
+ public String getToolTipText() {
+ return mStorage.getFullPath() != null ? mStorage.getFullPath().toString() : mStorage
+ .getName();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.runtime.IAdaptable#getAdapter(Class)
+ */
+ @Override
+ public Object getAdapter(Class adapter) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof XmlStorageEditorInput) {
+ return mStorage.equals(((XmlStorageEditorInput) obj).mStorage);
+ }
+ return super.equals(obj);
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return mStorage.hashCode();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorContentAssist.java
new file mode 100644
index 000000000..15704393e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorContentAssist.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.color;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+/**
+ * Content Assist Processor for /res/color XML files
+ */
+@VisibleForTesting
+public final class ColorContentAssist extends AndroidContentAssist {
+ public ColorContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_COLOR);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorDescriptors.java
new file mode 100644
index 000000000..16add3ec9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorDescriptors.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.color;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.eclipse.adt.internal.editors.animator.AnimatorDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.resources.ResourceType;
+
+import java.util.Map;
+
+/** Descriptors for /res/color XML files */
+public class ColorDescriptors implements IDescriptorProvider {
+ private static final String SDK_URL =
+ "http://d.android.com/guide/topics/resources/color-list-resource.html"; //$NON-NLS-1$
+
+ public static final String SELECTOR_TAG = "selector"; //$NON-NLS-1$
+ public static final String ATTR_COLOR = "color"; //$NON-NLS-1$
+
+ /** The root element descriptor */
+ private ElementDescriptor mDescriptor = new ElementDescriptor(
+ SELECTOR_TAG, "Selector",
+ "Required. This must be the root element. Contains one or more <item> elements.",
+ SDK_URL,
+ new AttributeDescriptor[] {
+ new XmlnsAttributeDescriptor(ANDROID_NS_NAME, ANDROID_URI) },
+ null /*children: added later*/, true /*mandatory*/);
+
+ /** @return the root descriptor. */
+ @Override
+ public ElementDescriptor getDescriptor() {
+ if (mDescriptor == null) {
+ mDescriptor = new ElementDescriptor("", getRootElementDescriptors()); //$NON-NLS-1$
+ }
+
+ return mDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return new ElementDescriptor[] { mDescriptor };
+ }
+
+ public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) {
+ if (styleMap == null) {
+ return;
+ }
+
+ // Selector children
+ ElementDescriptor selectorItem = AnimatorDescriptors.addElement(null, styleMap,
+ "item", "Item", "DrawableStates", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "Defines a drawable to use during certain states, as described by "
+ + "its attributes. Must be a child of a <selector> element.",
+ SDK_URL,
+ new ReferenceAttributeDescriptor(
+ ResourceType.COLOR, ATTR_COLOR,
+ ANDROID_URI,
+ new AttributeInfo(ATTR_COLOR, Format.COLOR_SET)).setTooltip(
+ "Hexadeximal color. Required. The color is specified with an RGB value and "
+ + "optional alpha channel.\n"
+ + "The value always begins with a pound (#) character and then "
+ + "followed by the Alpha-Red-Green-Blue information in one of "
+ + "the following formats:\n"
+ + "* RGB\n"
+ + "* ARGB\n"
+ + "* RRGGBB\n"
+ + "* AARRGGBB"),
+ null, /* This is wrong -- we can now embed any above drawable
+ (but without xmlns as extra) */
+ false /*mandatory*/);
+
+ if (selectorItem != null) {
+ mDescriptor.setChildren(new ElementDescriptor[] { selectorItem });
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorEditorDelegate.java
new file mode 100644
index 000000000..33896834c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/color/ColorEditorDelegate.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.color;
+
+import static com.android.ide.eclipse.adt.AdtConstants.EDITORS_NAMESPACE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Editor for /res/color XML files.
+ */
+@SuppressWarnings("restriction")
+public class ColorEditorDelegate extends CommonXmlDelegate {
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public ColorEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.COLOR == type) {
+ return new ColorEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ EDITORS_NAMESPACE + ".color.ColorEditor"; //$NON-NLS-1$
+
+
+ private ColorEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new ColorContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ @Override
+ public void delegateCreateFormPages() {
+ /* Disabled for now; doesn't work quite right
+ try {
+ addPage(new ColorTreePage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(IStatus.ERROR, "Error creating nested page"); //$NON-NLS-1$
+ AdtPlugin.getDefault().getLog().log(e.getStatus());
+ }
+ */
+ }
+
+ @Override
+ public void delegateXmlModelChanged(Document xmlDoc) {
+ // create the ui root node on demand.
+ delegateInitUiRootNode(false /*force*/);
+
+ Element rootElement = xmlDoc.getDocumentElement();
+ getUiRootNode().loadFromXmlNode(rootElement);
+ }
+
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (getUiRootNode() == null || force) {
+ ElementDescriptor descriptor;
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data == null) {
+ descriptor = new ColorDescriptors().getDescriptor();
+ } else {
+ descriptor = data.getColorDescriptors().getDescriptor();
+ }
+ setUiRootNode(descriptor.createUiNode());
+ getUiRootNode().setEditor(getEditor());
+ onDescriptorsChanged();
+ }
+ }
+
+ private void onDescriptorsChanged() {
+ IStructuredModel model = getEditor().getModelForRead();
+ if (model != null) {
+ try {
+ Node node = getEditor().getXmlDocument(model).getDocumentElement();
+ getUiRootNode().reloadFromXmlNode(node);
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonActionContributor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonActionContributor.java
new file mode 100755
index 000000000..5857b1532
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonActionContributor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.common;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.part.EditorActionBarContributor;
+
+/**
+ * Action contributor for the editors.
+ * This delegates to editor-specific action contributors.
+ */
+public class CommonActionContributor extends EditorActionBarContributor {
+
+ public CommonActionContributor() {
+ super();
+ }
+
+ @Override
+ public void setActiveEditor(IEditorPart part) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(part);
+ if (delegate != null) {
+ delegate.setActiveEditor(part, getActionBars());
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java
new file mode 100755
index 000000000..224c28fff
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.common;
+
+import static com.android.SdkConstants.FD_RES_LAYOUT;
+
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorMatchingStrategy;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorMatchingStrategy;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Matching strategy for the editors.
+ * This finds the right MatchingStrategy and delegates to it.
+ */
+public class CommonMatchingStrategy implements IEditorMatchingStrategy {
+
+ @Override
+ public boolean matches(IEditorReference editorRef, IEditorInput input) {
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+
+ // get the IFile object and check it's in one of the layout folders.
+ IFile file = fileInput.getFile();
+ if (file.getParent().getName().startsWith(FD_RES_LAYOUT)) {
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT) {
+ if (AdtPrefs.getPrefs().isSharedLayoutEditor()) {
+ LayoutEditorMatchingStrategy m = new LayoutEditorMatchingStrategy();
+ return m.matches(editorRef, fileInput);
+ } else {
+ // Skip files that don't match by name (see below). However, for
+ // layout files we can't just use editorRef.getName(), since
+ // the name sometimes includes the parent folder name (when the
+ // files are in layout- folders.
+ if (!(editorRef.getName().endsWith(file.getName()) &&
+ editorRef.getId().equals(CommonXmlEditor.ID))) {
+ return false;
+ }
+ }
+ }
+ } else {
+ // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput()
+ // is expensive so try exclude files that definitely don't match, such
+ // as those with the wrong extension or wrong file name
+ if (!(file.getName().equals(editorRef.getName()) &&
+ editorRef.getId().equals(CommonXmlEditor.ID))) {
+ return false;
+ }
+ }
+
+ try {
+ return input.equals(editorRef.getEditorInput());
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonSourceViewerConfig.java
new file mode 100755
index 000000000..be31040c3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonSourceViewerConfig.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.common;
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidSourceViewerConfig;
+
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.source.ISourceViewer;
+
+
+
+/**
+ * Source Viewer Configuration for the Common XML editor.
+ * Everything is already generic and done in the base class.
+ * The base class will use the delegate to find out the proper content assist to use.
+ */
+public final class CommonSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ private final IContentAssistProcessor mContentAssist;
+
+ public CommonSourceViewerConfig() {
+ super();
+ mContentAssist = null;
+ }
+
+ public CommonSourceViewerConfig(IContentAssistProcessor contentAssist) {
+ super();
+ mContentAssist = contentAssist;
+ }
+
+
+ /**
+ * @return The {@link IContentAssistProcessor} passed to the constructor or null.
+ */
+ @Override
+ public IContentAssistProcessor getAndroidContentAssistProcessor(
+ ISourceViewer sourceViewer,
+ String partitionType) {
+ // You may think you could use AndroidXmlEditor.fromTextViewer(sourceViewer)
+ // to find the editor associated with the sourceViewer and then access the
+ // delegate and query the content assist specific to a given delegate.
+ // Unfortunately this is invoked whilst the editor part is being created
+ // so we can't match an existing editor to the source view -- since there
+ // is no such "existing" editor. It's just being created.
+ //
+ // As a workaround, CommonXmlEditor#addPages() will unconfigure the
+ // default sourceViewerConfig and reconfigure it with one that really
+ // knows which content assist it should be using.
+
+ return mContentAssist;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java
new file mode 100755
index 000000000..d9ee8a5d8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlDelegate.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.common;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IURIEditorInput;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.part.EditorActionBarContributor;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.MultiPageEditorPart;
+import org.w3c.dom.Document;
+
+/**
+ * Implementation of form editor for /res XML files.
+ * <p/>
+ * All delegates must have one {@link IDelegateCreator} instance
+ * registered in the {@code DELEGATES[]} array of {@link CommonXmlEditor}.
+ */
+public abstract class CommonXmlDelegate {
+
+ /** The editor that created the delegate. Never null. */
+ private final CommonXmlEditor mEditor;
+
+ /** Root node of the UI element hierarchy. Can be null. */
+ private UiElementNode mUiRootNode;
+
+ private IContentAssistProcessor mContentAssist;
+
+ /**
+ * Static creator for {@link CommonXmlDelegate}s. Delegates implement a method
+ * that will decide whether this delegate can be created for the given file input.
+ */
+ public interface IDelegateCreator {
+ /**
+ * Determines whether this delegate can handle the given file, typically
+ * based on its resource path (e.g. ResourceManager#getResourceFolder).
+ *
+ * @param delegator The non-null instance of {@link CommonXmlEditor}.
+ * @param type The {@link ResourceFolderType} of the folder containing the file,
+ * if it can be determined. Null otherwise.
+ * @return A new delegate that can handle that file or null.
+ */
+ public @Nullable <T extends CommonXmlDelegate> T createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type);
+ }
+
+ /** Implemented by delegates that need to support {@link EditorActionBarContributor} */
+ public interface IActionContributorDelegate {
+ /** Called from {@link EditorActionBarContributor#setActiveEditor(IEditorPart)}. */
+ public void setActiveEditor(IEditorPart part, IActionBars bars);
+ }
+
+ protected CommonXmlDelegate(
+ CommonXmlEditor editor,
+ IContentAssistProcessor contentAssist) {
+ mEditor = editor;
+ mContentAssist = contentAssist;
+ }
+
+ public void dispose() {
+ }
+
+ /**
+ * Returns the editor that created this delegate.
+ *
+ * @return the editor that created this delegate. Never null.
+ */
+ public @NonNull CommonXmlEditor getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * @return The root node of the UI element hierarchy
+ */
+ public UiElementNode getUiRootNode() {
+ return mUiRootNode;
+ }
+
+ protected void setUiRootNode(UiElementNode uiRootNode) {
+ mUiRootNode = uiRootNode;
+ }
+
+ /** Called to compute the initial {@code UiRootNode}. */
+ public abstract void delegateInitUiRootNode(boolean force);
+
+ /**
+ * Returns true, indicating the "save as" operation is supported by this editor.
+ */
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ public abstract void delegateCreateFormPages();
+
+ public void delegatePostCreatePages() {
+ // pass
+ }
+
+ /**
+ * Changes the tab/title name to include the project name.
+ */
+ public void delegateSetInput(IEditorInput input) {
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ getEditor().setPartName(file.getName());
+ } else if (input instanceof IURIEditorInput) {
+ IURIEditorInput uriInput = (IURIEditorInput) input;
+ String name = uriInput.getName();
+ getEditor().setPartName(name);
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ public abstract void delegateXmlModelChanged(Document xml_doc);
+
+ public void delegatePageChange(int newPageIndex) {
+ // pass
+ }
+
+ public void delegatePostPageChange(int newPageIndex) {
+ // pass
+ }
+ /**
+ * Save the XML.
+ * <p/>
+ * The actual save operation is done in the super class by committing
+ * all data to the XML model and then having the Structured XML Editor
+ * save the XML.
+ * <p/>
+ * Here we just need to tell the graphical editor that the model has
+ * been saved.
+ */
+ public void delegateDoSave(IProgressMonitor monitor) {
+ // pass
+ }
+
+ /**
+ * Tells the editor to start a Lint check.
+ * It's up to the caller to check whether this should be done depending on preferences.
+ */
+ public Job delegateRunLint() {
+ return getEditor().startLintJob();
+ }
+
+
+ /**
+ * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
+ */
+ public Object delegateGetAdapter(Class<?> adapter) {
+ return null;
+ }
+
+ /**
+ * Returns the {@link IContentAssistProcessor} associated with this editor.
+ * Most implementations should lazily allocate one processor and always return the
+ * same instance.
+ * Must return null if there's no specific content assist processor for this editor.
+ */
+ public IContentAssistProcessor getAndroidContentAssistProcessor() {
+ return mContentAssist;
+ }
+
+ /**
+ * Does this editor participate in the "format GUI editor changes" option?
+ *
+ * @return false since editors do not support automatically formatting XML
+ * affected by GUI changes unless they explicitly opt in to it.
+ */
+ public boolean delegateSupportsFormatOnGuiEdit() {
+ return false;
+ }
+
+ /**
+ * Called after the editor's active page has been set.
+ *
+ * @param superReturned the return value from
+ * {@link MultiPageEditorPart#setActivePage(int)}
+ * @param pageIndex the index of the page to be activated; the index must be
+ * valid
+ * @return the page, or null
+ * @see MultiPageEditorPart#setActivePage(int)
+ */
+ public IFormPage delegatePostSetActivePage(IFormPage superReturned, String pageIndex) {
+ return superReturned;
+ }
+
+ /** Called after an editor has been activated */
+ public void delegateActivated() {
+ }
+
+ /** Called after an editor has been deactivated */
+ public void delegateDeactivated() {
+ }
+
+ /**
+ * Returns the name of the editor to be shown in the editor tab etc. Return
+ * null to keep the default.
+ *
+ * @return the part name, or null to use the default
+ */
+ public String delegateGetPartName() {
+ return null;
+ }
+
+ /**
+ * Returns the persistence category, as described in
+ * {@link AndroidXmlEditor#getPersistenceCategory}.
+ *
+ * @return the persistence category to use for this editor
+ */
+ public int delegateGetPersistenceCategory() {
+ return AndroidXmlEditor.CATEGORY_OTHER;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java
new file mode 100755
index 000000000..be06d38e2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonXmlEditor.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.common;
+
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.animator.AnimationEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.color.ColorEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate.IDelegateCreator;
+import com.android.ide.eclipse.adt.internal.editors.drawable.DrawableEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.menu.MenuEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.otherxml.OtherXmlEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.otherxml.PlainXmlEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.values.ValuesEditorDelegate;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.ISourceViewerExtension2;
+import org.eclipse.ui.IEditorDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IShowEditorInput;
+import org.eclipse.ui.IURIEditorInput;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.ide.IDE;
+import org.w3c.dom.Document;
+
+/**
+ * Multi-page form editor for ALL /res XML files.
+ * <p/>
+ * This editor doesn't actually do anything. Instead, it defers actual implementation
+ * to {@link CommonXmlDelegate} instances.
+ */
+public class CommonXmlEditor extends AndroidXmlEditor implements IShowEditorInput {
+
+ public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".CommonXmlEditor"; //$NON-NLS-1$
+
+ /**
+ * Registered {@link CommonXmlDelegate}s.
+ * All delegates must have a {@code Creator} class which is instantiated
+ * once here statically. All the creators are invoked in the order they
+ * are defined and the first one to return a non-null delegate is used.
+ */
+ private static final IDelegateCreator[] DELEGATES = {
+ new LayoutEditorDelegate.Creator(),
+ new ValuesEditorDelegate.Creator(),
+ new AnimationEditorDelegate.Creator(),
+ new ColorEditorDelegate.Creator(),
+ new DrawableEditorDelegate.Creator(),
+ new MenuEditorDelegate.Creator(),
+ new OtherXmlEditorDelegate.Creator(),
+ };
+
+ /**
+ * IDs of legacy editors replaced by the {@link CommonXmlEditor}.
+ */
+ public static final String[] LEGACY_EDITOR_IDS = {
+ LayoutEditorDelegate.LEGACY_EDITOR_ID,
+ ValuesEditorDelegate.LEGACY_EDITOR_ID,
+ AnimationEditorDelegate.LEGACY_EDITOR_ID,
+ ColorEditorDelegate.LEGACY_EDITOR_ID,
+ DrawableEditorDelegate.LEGACY_EDITOR_ID,
+ MenuEditorDelegate.LEGACY_EDITOR_ID,
+ OtherXmlEditorDelegate.LEGACY_EDITOR_ID,
+ };
+
+ private CommonXmlDelegate mDelegate = null;
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public CommonXmlEditor() {
+ super();
+ }
+
+ @Override
+ public void init(IEditorSite site, final IEditorInput editorInput)
+ throws PartInitException {
+ if (editorInput instanceof IFileEditorInput) {
+
+ IFileEditorInput fileInput = (IFileEditorInput) editorInput;
+ IFile file = fileInput.getFile();
+
+ // Adjust the default file editor ID
+
+ IEditorDescriptor file_desc = IDE.getDefaultEditor(file);
+ String id = file_desc == null ? null : file_desc.getId();
+ boolean mustChange = id != null &&
+ !id.equals(ID) &&
+ id.startsWith(AdtConstants.EDITORS_NAMESPACE);
+ if (!mustChange) {
+ // Maybe this was opened by a manual Open With with a legacy ID?
+ id = site.getId();
+ mustChange = id != null &&
+ !id.equals(ID) &&
+ id.startsWith(AdtConstants.EDITORS_NAMESPACE);
+ }
+
+ if (mustChange) {
+ // It starts by our editor namespace but it's not the right ID.
+ // This is an old Android XML ID. Change it to our new ID.
+ IDE.setDefaultEditor(file, ID);
+ AdtPlugin.log(IStatus.INFO,
+ "Changed legacy editor ID %s for %s", //$NON-NLS-1$
+ id,
+ file.getFullPath());
+ }
+
+ // Now find the delegate for the file.
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ ResourceFolderType type = resFolder == null ? null : resFolder.getType();
+
+ if (type == null) {
+ // We lack any real resource information about that file.
+ // Let's take a guess using the actual path.
+ String folderName = AdtUtils.getParentFolderName(editorInput);
+ type = ResourceFolderType.getFolderType(folderName);
+ }
+
+ if (type != null) {
+ for (IDelegateCreator creator : DELEGATES) {
+ mDelegate = creator.createForFile(this, type);
+ if (mDelegate != null) {
+ break;
+ }
+ }
+ }
+
+ if (mDelegate == null) {
+ // We didn't find any editor.
+ // We'll use the PlainXmlEditorDelegate as a catch-all editor.
+ AdtPlugin.log(IStatus.INFO,
+ "No valid Android XML Editor Delegate found for file %1$s [Res %2$s, type %3$s]",
+ file.getFullPath(),
+ resFolder,
+ type);
+ mDelegate = new PlainXmlEditorDelegate(this);
+ }
+ } else if (editorInput instanceof IURIEditorInput) {
+ String folderName = AdtUtils.getParentFolderName(editorInput);
+ ResourceFolderType type = ResourceFolderType.getFolderType(folderName);
+ if (type == ResourceFolderType.LAYOUT) {
+ // The layout editor has a lot of hardcoded requirements for real IFiles
+ // and IProjects so for now just use a plain XML editor for project-less layout
+ // files
+ mDelegate = new OtherXmlEditorDelegate(this);
+ } else if (type != null) {
+ for (IDelegateCreator creator : DELEGATES) {
+ mDelegate = creator.createForFile(this, type);
+ if (mDelegate != null) {
+ break;
+ }
+ }
+ }
+
+ if (mDelegate == null) {
+ // We didn't find any editor.
+ // We'll use the PlainXmlEditorDelegate as a catch-all editor.
+ AdtPlugin.log(IStatus.INFO,
+ "No valid Android XML Editor Delegate found for file %1$s [Res %2$s, type %3$s]",
+ ((IURIEditorInput) editorInput).getURI().toString(),
+ folderName,
+ type);
+ mDelegate = new PlainXmlEditorDelegate(this);
+ }
+ }
+
+ if (mDelegate == null) {
+ // We can't do anything if we don't have a valid file.
+ AdtPlugin.log(IStatus.INFO,
+ "Android XML Editor cannot process non-file input %1$s", //$NON-NLS-1$
+ (editorInput == null ? "null" : editorInput.toString())); //$NON-NLS-1$
+ throw new PartInitException("Android XML Editor cannot process this input.");
+ } else {
+ // Invoke the editor's init after setting up the delegate. This will call setInput().
+ super.init(site, editorInput);
+ }
+ }
+
+ /**
+ * @return The root node of the UI element hierarchy
+ */
+ @Override
+ public UiElementNode getUiRootNode() {
+ return mDelegate == null ? null : mDelegate.getUiRootNode();
+ }
+
+ public CommonXmlDelegate getDelegate() {
+ return mDelegate;
+ }
+
+ // ---- Base Class Overrides ----
+
+ @Override
+ public void dispose() {
+ if (mDelegate != null) {
+ mDelegate.dispose();
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Save the XML.
+ * <p/>
+ * The actual save operation is done in the super class by committing
+ * all data to the XML model and then having the Structured XML Editor
+ * save the XML.
+ * <p/>
+ * Here we just need to tell the delegate that the model has
+ * been saved.
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ super.doSave(monitor);
+ if (mDelegate != null) {
+ mDelegate.delegateDoSave(monitor);
+ }
+ }
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return mDelegate == null ? false : mDelegate.isSaveAsAllowed();
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ if (mDelegate != null) {
+ mDelegate.delegateCreateFormPages();
+ }
+ }
+
+ @Override
+ protected void postCreatePages() {
+ super.postCreatePages();
+
+ if (mDelegate != null) {
+ mDelegate.delegatePostCreatePages();
+ }
+ }
+
+ @Override
+ protected void addPages() {
+ // Create the editor pages.
+ // This will also create the EditorPart.
+ super.addPages();
+
+ // When the EditorPart is being created, it configures the SourceViewer
+ // and will try to use our CommonSourceViewerConfig. Our config needs to
+ // know which ContentAssist processor to use (since we have one per resource
+ // folder type) but it doesn't have the necessary info to do so.
+ // Consequently, once the part is created, we can now unconfigure the source
+ // viewer and reconfigure it with the right settings.
+ ISourceViewer ssv = getStructuredSourceViewer();
+ if (mDelegate != null && ssv instanceof ISourceViewerExtension2) {
+ ((ISourceViewerExtension2) ssv).unconfigure();
+ ssv.configure(new CommonSourceViewerConfig(
+ mDelegate.getAndroidContentAssistProcessor()));
+ }
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the name of the layout.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ assert mDelegate != null;
+ if (mDelegate != null) {
+ mDelegate.delegateSetInput(input);
+ }
+ }
+
+ @Override
+ public void setInputWithNotify(IEditorInput input) {
+ super.setInputWithNotify(input);
+ if (mDelegate instanceof LayoutEditorDelegate) {
+ ((LayoutEditorDelegate) mDelegate).delegateSetInputWithNotify(input);
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ if (mDelegate != null) {
+ mDelegate.delegateXmlModelChanged(xml_doc);
+ }
+ }
+
+ @Override
+ protected Job runLint() {
+ if (mDelegate != null && getEditorInput() instanceof IFileEditorInput) {
+ return mDelegate.delegateRunLint();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
+ */
+ @Override
+ public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+ if (mDelegate != null) {
+ Object value = mDelegate.delegateGetAdapter(adapter);
+ if (value != null) {
+ return value;
+ }
+ }
+
+ // return default
+ return super.getAdapter(adapter);
+ }
+
+ @Override
+ protected void pageChange(int newPageIndex) {
+ if (mDelegate != null) {
+ mDelegate.delegatePageChange(newPageIndex);
+ }
+
+ super.pageChange(newPageIndex);
+
+ if (mDelegate != null) {
+ mDelegate.delegatePostPageChange(newPageIndex);
+ }
+ }
+
+ @Override
+ protected int getPersistenceCategory() {
+ if (mDelegate != null) {
+ return mDelegate.delegateGetPersistenceCategory();
+ }
+ return CATEGORY_OTHER;
+ }
+
+ @Override
+ public void initUiRootNode(boolean force) {
+ if (mDelegate != null) {
+ mDelegate.delegateInitUiRootNode(force);
+ }
+ }
+
+ @Override
+ public IFormPage setActivePage(String pageId) {
+ IFormPage page = super.setActivePage(pageId);
+
+ if (mDelegate != null) {
+ return mDelegate.delegatePostSetActivePage(page, pageId);
+ }
+
+ return page;
+ }
+
+ /* Implements showEditorInput(...) in IShowEditorInput */
+ @Override
+ public void showEditorInput(IEditorInput editorInput) {
+ if (mDelegate instanceof LayoutEditorDelegate) {
+ ((LayoutEditorDelegate) mDelegate).showEditorInput(editorInput);
+ }
+ }
+
+ @Override
+ public boolean supportsFormatOnGuiEdit() {
+ if (mDelegate != null) {
+ return mDelegate.delegateSupportsFormatOnGuiEdit();
+ }
+ return super.supportsFormatOnGuiEdit();
+ }
+
+ @Override
+ public void activated() {
+ super.activated();
+ if (mDelegate != null) {
+ mDelegate.delegateActivated();
+ }
+ }
+
+ @Override
+ public void deactivated() {
+ super.deactivated();
+ if (mDelegate != null) {
+ mDelegate.delegateDeactivated();
+ }
+ }
+
+ @Override
+ public String getPartName() {
+ if (mDelegate != null) {
+ String name = mDelegate.delegateGetPartName();
+ if (name != null) {
+ return name;
+ }
+ }
+
+ return super.getPartName();
+ }
+
+ // --------------------
+ // Base methods exposed so that XmlEditorDelegate can access them
+
+ @Override
+ public void setPartName(String partName) {
+ super.setPartName(partName);
+ }
+
+ @Override
+ public void setPageText(int pageIndex, String text) {
+ super.setPageText(pageIndex, text);
+ }
+
+ @Override
+ public void firePropertyChange(int propertyId) {
+ super.firePropertyChange(propertyId);
+ }
+
+ @Override
+ public int getPageCount() {
+ return super.getPageCount();
+ }
+
+ @Override
+ public int getCurrentPage() {
+ return super.getCurrentPage();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptor.java
new file mode 100644
index 000000000..345a109e6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptor.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * {@link AttributeDescriptor} describes an XML attribute with its XML attribute name.
+ * <p/>
+ * An attribute descriptor also knows which UI node should be instantiated to represent
+ * this particular attribute (e.g. text field, icon chooser, class selector, etc.)
+ * Some attributes may be hidden and have no user interface at all.
+ * <p/>
+ * This is an abstract class. Derived classes must implement data description and return
+ * the correct UiAttributeNode-derived class.
+ */
+public abstract class AttributeDescriptor implements Comparable<AttributeDescriptor> {
+ public static final String ATTRIBUTE_ICON_FILENAME = "attribute"; //$NON-NLS-1$
+
+ private final String mXmlLocalName;
+ private final String mNsUri;
+ private final IAttributeInfo mAttrInfo;
+ private ElementDescriptor mParent;
+
+ /**
+ * Creates a new {@link AttributeDescriptor}
+ *
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null for a "real"
+ * attribute representing a View element's attribute. Can be null for some
+ * specialized internal attribute descriptors (e.g. hidden descriptors, XMLNS,
+ * or attribute separator, all of which do not represent any real attribute.)
+ */
+ public AttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ assert xmlLocalName != null;
+ mXmlLocalName = xmlLocalName;
+ mNsUri = nsUri;
+ mAttrInfo = attrInfo;
+ }
+
+ /** Returns the XML local name of the attribute (case sensitive). */
+ public final String getXmlLocalName() {
+ return mXmlLocalName;
+ }
+
+ /** Returns the namespace URI of this attribute. */
+ public final String getNamespaceUri() {
+ return mNsUri;
+ }
+
+ /** Sets the element descriptor to which this attribute is attached. */
+ final void setParent(ElementDescriptor parent) {
+ mParent = parent;
+ }
+
+ /** Returns the element descriptor to which this attribute is attached. */
+ public final ElementDescriptor getParent() {
+ return mParent;
+ }
+
+ /** Returns whether this attribute is deprecated (based on its attrs.xml javadoc.) */
+ public boolean isDeprecated() {
+ return mAttrInfo == null ? false : mAttrInfo.getDeprecatedDoc() != null;
+ }
+
+ /**
+ * Returns the {@link IAttributeInfo} of this attribute.
+ * Can't be null for real attributes.
+ * Can be null for specialized internal attribute descriptors that do not correspond to
+ * any real XML attribute.
+ */
+ public IAttributeInfo getAttributeInfo() {
+ return mAttrInfo;
+ }
+
+ /**
+ * Returns an optional icon for the attribute.
+ * This icon is generic, that is all attribute descriptors have the same icon
+ * no matter what they represent.
+ *
+ * @return An icon for this element or null.
+ */
+ public Image getGenericIcon() {
+ return IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME);
+ }
+
+ /**
+ * @param uiParent The {@link UiElementNode} parent of this UI attribute.
+ * @return A new {@link UiAttributeNode} linked to this descriptor or null if this
+ * attribute has no user interface.
+ */
+ public abstract UiAttributeNode createUiNode(UiElementNode uiParent);
+
+ // Implements Comparable<AttributeDescriptor>
+ @Override
+ public int compareTo(AttributeDescriptor other) {
+ return mXmlLocalName.compareTo(other.mXmlLocalName);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptorLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptorLabelProvider.java
new file mode 100644
index 000000000..32def6456
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptorLabelProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAbstractTextAttributeNode;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Label provider for {@link UiAbstractTextAttributeNode}.
+ */
+public class AttributeDescriptorLabelProvider implements ILabelProvider {
+
+ private final static AttributeDescriptorLabelProvider sThis =
+ new AttributeDescriptorLabelProvider();
+
+ public static ILabelProvider getProvider() {
+ return sThis;
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ if (element instanceof UiAbstractTextAttributeNode) {
+ UiAbstractTextAttributeNode node = (UiAbstractTextAttributeNode) element;
+ if (node.getDescriptor().isDeprecated()) {
+ String v = node.getCurrentValue();
+ if (v != null && v.length() > 0) {
+ IconFactory factory = IconFactory.getInstance();
+ return factory.getIcon("warning"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getText(Object element) {
+ if (element instanceof UiAbstractTextAttributeNode) {
+ return ((UiAbstractTextAttributeNode)element).getCurrentValue();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void dispose() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/BooleanAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/BooleanAttributeDescriptor.java
new file mode 100644
index 000000000..7d76687c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/BooleanAttributeDescriptor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiListAttributeNode;
+
+/**
+ * Describes a text attribute that can only contain boolean values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ */
+public class BooleanAttributeDescriptor extends ListAttributeDescriptor {
+ private static final String[] VALUES = new String[] { "true", "false" }; //$NON-NLS-1$ //$NON-NLS-2$
+
+ public BooleanAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo, VALUES);
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
new file mode 100644
index 000000000..da3a1856c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
@@ -0,0 +1,961 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.EDIT_TEXT;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_ADAPTER_VIEW;
+import static com.android.SdkConstants.GALLERY;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.GT_ENTITY;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.LIST_VIEW;
+import static com.android.SdkConstants.LT_ENTITY;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.RELATIVE_LAYOUT;
+import static com.android.SdkConstants.REQUEST_FOCUS;
+import static com.android.SdkConstants.SPACE;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.resources.ResourceType;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * Utility methods related to descriptors handling.
+ */
+public final class DescriptorsUtils {
+ private static final String DEFAULT_WIDGET_PREFIX = "widget";
+
+ private static final int JAVADOC_BREAK_LENGTH = 60;
+
+ /**
+ * The path in the online documentation for the manifest description.
+ * <p/>
+ * This is NOT a complete URL. To be used, it needs to be appended
+ * to {@link AdtConstants#CODESITE_BASE_URL} or to the local SDK
+ * documentation.
+ */
+ public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#"; //$NON-NLS-1$
+
+ public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
+
+ private static final String CODE = "$code"; //$NON-NLS-1$
+ private static final String LINK = "$link"; //$NON-NLS-1$
+ private static final String ELEM = "$elem"; //$NON-NLS-1$
+ private static final String BREAK = "$break"; //$NON-NLS-1$
+
+ /**
+ * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to append to
+ * @param elementXmlName Optional XML local name of the element to which attributes are
+ * being added. When not null, this is used to filter overrides.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param infos The array of {@link AttributeInfo} to read and append to attributes
+ * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append
+ * a "*" to their UI name as a hint for the user.) If not null, must contains
+ * entries in the form "elem-name/attr-name". Elem-name can be "*".
+ * @param overrides A map [attribute name => ITextAttributeCreator creator].
+ */
+ public static void appendAttributes(List<AttributeDescriptor> attributes,
+ String elementXmlName,
+ String nsUri, AttributeInfo[] infos,
+ Set<String> requiredAttributes,
+ Map<String, ITextAttributeCreator> overrides) {
+ for (AttributeInfo info : infos) {
+ boolean required = false;
+ if (requiredAttributes != null) {
+ String attr_name = info.getName();
+ if (requiredAttributes.contains("*/" + attr_name) ||
+ requiredAttributes.contains(elementXmlName + "/" + attr_name)) {
+ required = true;
+ }
+ }
+ appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides);
+ }
+ }
+
+ /**
+ * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to append to
+ * @param elementXmlName Optional XML local name of the element to which attributes are
+ * being added. When not null, this is used to filter overrides.
+ * @param info The {@link AttributeInfo} to append to attributes
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param required True if the attribute is to be marked as "required" (i.e. append
+ * a "*" to its UI name as a hint for the user.)
+ * @param overrides A map [attribute name => ITextAttributeCreator creator].
+ */
+ public static void appendAttribute(List<AttributeDescriptor> attributes,
+ String elementXmlName,
+ String nsUri,
+ AttributeInfo info, boolean required,
+ Map<String, ITextAttributeCreator> overrides) {
+ TextAttributeDescriptor attr = null;
+
+ String xmlLocalName = info.getName();
+
+ // Add the known types to the tooltip
+ EnumSet<Format> formats_set = info.getFormats();
+ int flen = formats_set.size();
+ if (flen > 0) {
+ // Create a specialized attribute if we can
+ if (overrides != null) {
+ for (Entry<String, ITextAttributeCreator> entry: overrides.entrySet()) {
+ // The override key can have the following formats:
+ // */xmlLocalName
+ // element/xmlLocalName
+ // element1,element2,...,elementN/xmlLocalName
+ String key = entry.getKey();
+ String elements[] = key.split("/"); //$NON-NLS-1$
+ String overrideAttrLocalName = null;
+ if (elements.length < 1) {
+ continue;
+ } else if (elements.length == 1) {
+ overrideAttrLocalName = elements[0];
+ elements = null;
+ } else {
+ overrideAttrLocalName = elements[elements.length - 1];
+ elements = elements[0].split(","); //$NON-NLS-1$
+ }
+
+ if (overrideAttrLocalName == null ||
+ !overrideAttrLocalName.equals(xmlLocalName)) {
+ continue;
+ }
+
+ boolean ok_element = elements != null && elements.length < 1;
+ if (!ok_element && elements != null) {
+ for (String element : elements) {
+ if (element.equals("*") //$NON-NLS-1$
+ || element.equals(elementXmlName)) {
+ ok_element = true;
+ break;
+ }
+ }
+ }
+
+ if (!ok_element) {
+ continue;
+ }
+
+ ITextAttributeCreator override = entry.getValue();
+ if (override != null) {
+ attr = override.create(xmlLocalName, nsUri, info);
+ }
+ }
+ } // if overrides
+
+ // Create a specialized descriptor if we can, based on type
+ if (attr == null) {
+ if (formats_set.contains(Format.REFERENCE)) {
+ // This is either a multi-type reference or a generic reference.
+ attr = new ReferenceAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.ENUM)) {
+ attr = new ListAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.FLAG)) {
+ attr = new FlagAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.BOOLEAN)) {
+ attr = new BooleanAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.STRING)) {
+ attr = new ReferenceAttributeDescriptor(
+ ResourceType.STRING, xmlLocalName, nsUri, info);
+ }
+ }
+ }
+
+ // By default a simple text field is used
+ if (attr == null) {
+ attr = new TextAttributeDescriptor(xmlLocalName, nsUri, info);
+ }
+
+ if (required) {
+ attr.setRequired(true);
+ }
+
+ attributes.add(attr);
+ }
+
+ /**
+ * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of
+ * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same
+ * XML name.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to compare to.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param info The {@link AttributeInfo} to know whether it is included in the above list.
+ * @return True if this {@link AttributeInfo} is already present in
+ * the {@link AttributeDescriptor} list.
+ */
+ public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes,
+ String nsUri,
+ AttributeInfo info) {
+ String xmlLocalName = info.getName();
+ for (AttributeDescriptor desc : attributes) {
+ if (desc.getXmlLocalName().equals(xmlLocalName)) {
+ if (nsUri == desc.getNamespaceUri() ||
+ (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Create a pretty attribute UI name from an XML name.
+ * <p/>
+ * The original xml name starts with a lower case and is camel-case,
+ * e.g. "maxWidthForView". The pretty name starts with an upper case
+ * and has space separators, e.g. "Max width for view".
+ */
+ public static String prettyAttributeUiName(String name) {
+ if (name.length() < 1) {
+ return name;
+ }
+ StringBuilder buf = new StringBuilder(2 * name.length());
+
+ char c = name.charAt(0);
+ // Use upper case initial letter
+ buf.append(Character.toUpperCase(c));
+ int len = name.length();
+ for (int i = 1; i < len; i++) {
+ c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ // Break camel case into separate words
+ buf.append(' ');
+ // Use a lower case initial letter for the next word, except if the
+ // word is solely X, Y or Z.
+ if (c >= 'X' && c <= 'Z' &&
+ (i == len-1 ||
+ (i < len-1 && Character.isUpperCase(name.charAt(i+1))))) {
+ buf.append(c);
+ } else {
+ buf.append(Character.toLowerCase(c));
+ }
+ } else if (c == '_') {
+ buf.append(' ');
+ } else {
+ buf.append(c);
+ }
+ }
+
+ name = buf.toString();
+
+ name = replaceAcronyms(name);
+
+ return name;
+ }
+
+ /**
+ * Similar to {@link #prettyAttributeUiName(String)}, but it will capitalize
+ * all words, not just the first one.
+ * <p/>
+ * The original xml name starts with a lower case and is camel-case, e.g.
+ * "maxWidthForView". The corresponding return value is
+ * "Max Width For View".
+ *
+ * @param name the attribute name, which should be a camel case name, e.g.
+ * "maxWidth"
+ * @return the corresponding display name, e.g. "Max Width"
+ */
+ @NonNull
+ public static String capitalize(@NonNull String name) {
+ if (name.isEmpty()) {
+ return name;
+ }
+ StringBuilder buf = new StringBuilder(2 * name.length());
+
+ char c = name.charAt(0);
+ // Use upper case initial letter
+ buf.append(Character.toUpperCase(c));
+ int len = name.length();
+ for (int i = 1; i < len; i++) {
+ c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ // Break camel case into separate words
+ buf.append(' ');
+ // Use a lower case initial letter for the next word, except if the
+ // word is solely X, Y or Z.
+ buf.append(c);
+ } else if (c == '_') {
+ buf.append(' ');
+ if (i < len -1 && Character.isLowerCase(name.charAt(i + 1))) {
+ buf.append(Character.toUpperCase(name.charAt(i + 1)));
+ i++;
+ }
+ } else {
+ buf.append(c);
+ }
+ }
+
+ name = buf.toString();
+
+ name = replaceAcronyms(name);
+
+ return name;
+ }
+
+ private static String replaceAcronyms(String name) {
+ // Replace these acronyms by upper-case versions
+ // - (?<=^| ) means "if preceded by a space or beginning of string"
+ // - (?=$| ) means "if followed by a space or end of string"
+ if (name.contains("sdk") || name.contains("Sdk")) {
+ name = name.replaceAll("(?<=^| )[sS]dk(?=$| )", "SDK");
+ }
+ if (name.contains("uri") || name.contains("Uri")) {
+ name = name.replaceAll("(?<=^| )[uU]ri(?=$| )", "URI");
+ }
+ if (name.contains("ime") || name.contains("Ime")) {
+ name = name.replaceAll("(?<=^| )[iI]me(?=$| )", "IME");
+ }
+ if (name.contains("vm") || name.contains("Vm")) {
+ name = name.replaceAll("(?<=^| )[vV]m(?=$| )", "VM");
+ }
+ if (name.contains("ui") || name.contains("Ui")) {
+ name = name.replaceAll("(?<=^| )[uU]i(?=$| )", "UI");
+ }
+ return name;
+ }
+
+ /**
+ * Formats the javadoc tooltip to be usable in a tooltip.
+ */
+ public static String formatTooltip(String javadoc) {
+ ArrayList<String> spans = scanJavadoc(javadoc);
+
+ StringBuilder sb = new StringBuilder();
+ boolean needBreak = false;
+
+ for (int n = spans.size(), i = 0; i < n; ++i) {
+ String s = spans.get(i);
+ if (CODE.equals(s)) {
+ s = spans.get(++i);
+ if (s != null) {
+ sb.append('"').append(s).append('"');
+ }
+ } else if (LINK.equals(s)) {
+ String base = spans.get(++i);
+ String anchor = spans.get(++i);
+ String text = spans.get(++i);
+
+ if (base != null) {
+ base = base.trim();
+ }
+ if (anchor != null) {
+ anchor = anchor.trim();
+ }
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // If there's no text, use the anchor if there's one
+ if (text == null || text.length() == 0) {
+ text = anchor;
+ }
+
+ if (base != null && base.length() > 0) {
+ if (text == null || text.length() == 0) {
+ // If we still have no text, use the base as text
+ text = base;
+ }
+ }
+
+ if (text != null) {
+ sb.append(text);
+ }
+
+ } else if (ELEM.equals(s)) {
+ s = spans.get(++i);
+ if (s != null) {
+ sb.append(s);
+ }
+ } else if (BREAK.equals(s)) {
+ needBreak = true;
+ } else if (s != null) {
+ if (needBreak && s.trim().length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(s);
+ needBreak = false;
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the javadoc tooltip to be usable in a FormText.
+ * <p/>
+ * If the descriptor can provide an icon, the caller should provide
+ * elementsDescriptor.getIcon() as "image" to FormText, e.g.:
+ * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code>
+ *
+ * @param javadoc The javadoc to format. Cannot be null.
+ * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null.
+ * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be
+ * <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code>
+ */
+ public static String formatFormText(String javadoc,
+ ElementDescriptor elementDescriptor,
+ String androidDocBaseUrl) {
+ ArrayList<String> spans = scanJavadoc(javadoc);
+
+ String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL;
+ String sdkUrl = elementDescriptor.getSdkUrl();
+ if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) {
+ fullSdkUrl = androidDocBaseUrl + sdkUrl;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ Image icon = elementDescriptor.getCustomizedIcon();
+ if (icon != null) {
+ sb.append("<form><li style=\"image\" value=\"" + //$NON-NLS-1$
+ IMAGE_KEY + "\">"); //$NON-NLS-1$
+ } else {
+ sb.append("<form><p>"); //$NON-NLS-1$
+ }
+
+ for (int n = spans.size(), i = 0; i < n; ++i) {
+ String s = spans.get(i);
+ if (CODE.equals(s)) {
+ s = spans.get(++i);
+ if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(fullSdkUrl);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(s);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (s != null) {
+ sb.append('"').append(s).append('"');
+ }
+ } else if (LINK.equals(s)) {
+ String base = spans.get(++i);
+ String anchor = spans.get(++i);
+ String text = spans.get(++i);
+
+ if (base != null) {
+ base = base.trim();
+ }
+ if (anchor != null) {
+ anchor = anchor.trim();
+ }
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // If there's no text, use the anchor if there's one
+ if (text == null || text.length() == 0) {
+ text = anchor;
+ }
+
+ // TODO specialize with a base URL for views, menus & other resources
+ // Base is empty for a local page anchor, in which case we'll replace it
+ // by the element SDK URL if it exists.
+ if ((base == null || base.length() == 0) && fullSdkUrl != null) {
+ base = fullSdkUrl;
+ }
+
+ String url = null;
+ if (base != null && base.length() > 0) {
+ if (base.startsWith("http")) { //$NON-NLS-1$
+ // If base looks an URL, use it, with the optional anchor
+ url = base;
+ if (anchor != null && anchor.length() > 0) {
+ // If the base URL already has an anchor, it needs to be
+ // removed first. If there's no anchor, we need to add "#"
+ int pos = url.lastIndexOf('#');
+ if (pos < 0) {
+ url += "#"; //$NON-NLS-1$
+ } else if (pos < url.length() - 1) {
+ url = url.substring(0, pos + 1);
+ }
+
+ url += anchor;
+ }
+ } else if (text == null || text.length() == 0) {
+ // If we still have no text, use the base as text
+ text = base;
+ }
+ }
+
+ if (url != null && text != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(url);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(text);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (text != null) {
+ sb.append("<b>").append(text).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ } else if (ELEM.equals(s)) {
+ s = spans.get(++i);
+ if (sdkUrl != null && s != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(sdkUrl);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(s);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (s != null) {
+ sb.append("<b>").append(s).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ } else if (BREAK.equals(s)) {
+ // ignore line breaks in pseudo-HTML rendering
+ } else if (s != null) {
+ sb.append(s);
+ }
+ }
+
+ if (icon != null) {
+ sb.append("</li></form>"); //$NON-NLS-1$
+ } else {
+ sb.append("</p></form>"); //$NON-NLS-1$
+ }
+ return sb.toString();
+ }
+
+ private static ArrayList<String> scanJavadoc(String javadoc) {
+ ArrayList<String> spans = new ArrayList<String>();
+
+ // Standardize all whitespace in the javadoc to single spaces.
+ if (javadoc != null) {
+ javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ // Detects {@link <base>#<name> <text>} where all 3 are optional
+ Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$
+ // Detects <code>blah</code>
+ Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)"); //$NON-NLS-1$
+ // Detects @blah@, used in hard-coded tooltip descriptors
+ Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)"); //$NON-NLS-1$
+ // Detects a buffer that starts by @@ (request for a break)
+ Pattern p_break = Pattern.compile("@@(.*)"); //$NON-NLS-1$
+ // Detects a buffer that starts by @ < or { (one that was not matched above)
+ Pattern p_open = Pattern.compile("([@<\\{])(.*)"); //$NON-NLS-1$
+ // Detects everything till the next potential separator, i.e. @ < or {
+ Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)"); //$NON-NLS-1$
+
+ int currentLength = 0;
+ String text = null;
+
+ while(javadoc != null && javadoc.length() > 0) {
+ Matcher m;
+ String s = null;
+ if ((m = p_code.matcher(javadoc)).matches()) {
+ spans.add(CODE);
+ spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text
+ javadoc = m.group(2);
+ if (text != null) {
+ currentLength += text.length();
+ }
+ } else if ((m = p_link.matcher(javadoc)).matches()) {
+ spans.add(LINK);
+ spans.add(m.group(1)); // @link base
+ spans.add(m.group(2)); // @link anchor
+ spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text
+ javadoc = m.group(4);
+ if (text != null) {
+ currentLength += text.length();
+ }
+ } else if ((m = p_elem.matcher(javadoc)).matches()) {
+ spans.add(ELEM);
+ spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@
+ javadoc = m.group(2);
+ if (text != null) {
+ currentLength += text.length() - 2;
+ }
+ } else if ((m = p_break.matcher(javadoc)).matches()) {
+ spans.add(BREAK);
+ currentLength = 0;
+ javadoc = m.group(1);
+ } else if ((m = p_open.matcher(javadoc)).matches()) {
+ s = m.group(1);
+ javadoc = m.group(2);
+ } else if ((m = p_text.matcher(javadoc)).matches()) {
+ s = m.group(1);
+ javadoc = m.group(2);
+ } else {
+ // This is not supposed to happen. In case of, just use everything.
+ s = javadoc;
+ javadoc = null;
+ }
+ if (s != null && s.length() > 0) {
+ s = cleanupJavadocHtml(s);
+
+ if (currentLength >= JAVADOC_BREAK_LENGTH) {
+ spans.add(BREAK);
+ currentLength = 0;
+ }
+ while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) {
+ int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength);
+ if (pos <= 0) {
+ break;
+ }
+ spans.add(s.substring(0, pos + 1));
+ spans.add(BREAK);
+ currentLength = 0;
+ s = s.substring(pos + 1);
+ }
+
+ spans.add(s);
+ currentLength += s.length();
+ }
+ }
+
+ return spans;
+ }
+
+ /**
+ * Remove anything that looks like HTML from a javadoc snippet, as it is supported
+ * neither by FormText nor a standard text tooltip.
+ */
+ private static String cleanupJavadocHtml(String s) {
+ if (s != null) {
+ s = s.replaceAll(LT_ENTITY, "\""); //$NON-NLS-1$ $NON-NLS-2$
+ s = s.replaceAll(GT_ENTITY, "\""); //$NON-NLS-1$ $NON-NLS-2$
+ s = s.replaceAll("<[^>]+>", ""); //$NON-NLS-1$ $NON-NLS-2$
+ }
+ return s;
+ }
+
+ /**
+ * Returns the basename for the given fully qualified class name. It is okay to pass
+ * a basename to this method which will just be returned back.
+ *
+ * @param fqcn The fully qualified class name to convert
+ * @return the basename of the class name
+ */
+ public static String getBasename(String fqcn) {
+ String name = fqcn;
+ int lastDot = name.lastIndexOf('.');
+ if (lastDot != -1) {
+ name = name.substring(lastDot + 1);
+ }
+
+ return name;
+ }
+
+ /**
+ * Sets the default layout attributes for the a new UiElementNode.
+ * <p/>
+ * Note that ideally the node should already be part of a hierarchy so that its
+ * parent layout and previous sibling can be determined, if any.
+ * <p/>
+ * This does not override attributes which are not empty.
+ */
+ public static void setDefaultLayoutAttributes(UiElementNode node, boolean updateLayout) {
+ // if this ui_node is a layout and we're adding it to a document, use match_parent for
+ // both W/H. Otherwise default to wrap_layout.
+ ElementDescriptor descriptor = node.getDescriptor();
+
+ String name = descriptor.getXmlLocalName();
+ if (name.equals(REQUEST_FOCUS)) {
+ // Don't add ids, widths and heights etc to <requestFocus>
+ return;
+ }
+
+ // Width and height are mandatory in all layouts except GridLayout
+ boolean setSize = !node.getUiParent().getDescriptor().getXmlName().equals(GRID_LAYOUT);
+ if (setSize) {
+ boolean fill = descriptor.hasChildren() &&
+ node.getUiParent() instanceof UiDocumentNode;
+ node.setAttributeValue(
+ ATTR_LAYOUT_WIDTH,
+ ANDROID_URI,
+ fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
+ false /* override */);
+ node.setAttributeValue(
+ ATTR_LAYOUT_HEIGHT,
+ ANDROID_URI,
+ fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
+ false /* override */);
+ }
+
+ if (needsDefaultId(node.getDescriptor())) {
+ String freeId = getFreeWidgetId(node);
+ if (freeId != null) {
+ node.setAttributeValue(
+ ATTR_ID,
+ ANDROID_URI,
+ freeId,
+ false /* override */);
+ }
+ }
+
+ // Set a text attribute on textual widgets -- but only on those that define a text
+ // attribute
+ if (descriptor.definesAttribute(ANDROID_URI, ATTR_TEXT)
+ // Don't set default text value into edit texts - they typically start out blank
+ && !descriptor.getXmlLocalName().equals(EDIT_TEXT)) {
+ String type = getBasename(descriptor.getUiName());
+ node.setAttributeValue(
+ ATTR_TEXT,
+ ANDROID_URI,
+ type,
+ false /*override*/);
+ }
+
+ if (updateLayout) {
+ UiElementNode parent = node.getUiParent();
+ if (parent != null &&
+ parent.getDescriptor().getXmlLocalName().equals(
+ RELATIVE_LAYOUT)) {
+ UiElementNode previous = node.getUiPreviousSibling();
+ if (previous != null) {
+ String id = previous.getAttributeValue(ATTR_ID);
+ if (id != null && id.length() > 0) {
+ id = id.replace("@+", "@"); //$NON-NLS-1$ //$NON-NLS-2$
+ node.setAttributeValue(
+ ATTR_LAYOUT_BELOW,
+ ANDROID_URI,
+ id,
+ false /* override */);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines whether new views of the given type should be assigned a
+ * default id.
+ *
+ * @param descriptor a descriptor describing the view to look up
+ * @return true if new views of the given type should be assigned a default
+ * id
+ */
+ public static boolean needsDefaultId(ElementDescriptor descriptor) {
+ // By default, layouts do not need ids.
+ String tag = descriptor.getXmlLocalName();
+ if (tag.endsWith("Layout") //$NON-NLS-1$
+ || tag.equals(VIEW_INCLUDE)
+ || tag.equals(VIEW_MERGE)
+ || tag.equals(SPACE)
+ || tag.endsWith(SPACE) && tag.length() > SPACE.length() &&
+ tag.charAt(tag.length() - SPACE.length()) == '.') {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Given a UI node, returns the first available id that matches the
+ * pattern "prefix%d".
+ * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
+ *
+ * @param uiNode The UI node that gives the prefix to match.
+ * @return A suitable generated id in the attribute form needed by the XML id tag
+ * (e.g. "@+id/something")
+ */
+ public static String getFreeWidgetId(UiElementNode uiNode) {
+ String name = getBasename(uiNode.getDescriptor().getXmlLocalName());
+ return getFreeWidgetId(uiNode.getUiRoot(), name);
+ }
+
+ /**
+ * Given a UI root node and a potential XML node name, returns the first available
+ * id that matches the pattern "prefix%d".
+ * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
+ *
+ * @param uiRoot The root UI node to search for name conflicts from
+ * @param name The XML node prefix name to look for
+ * @return A suitable generated id in the attribute form needed by the XML id tag
+ * (e.g. "@+id/something")
+ */
+ public static String getFreeWidgetId(UiElementNode uiRoot, String name) {
+ if ("TabWidget".equals(name)) { //$NON-NLS-1$
+ return "@android:id/tabs"; //$NON-NLS-1$
+ }
+
+ return NEW_ID_PREFIX + getFreeWidgetId(uiRoot,
+ new Object[] { name, null, null, null });
+ }
+
+ /**
+ * Given a UI root node, returns the first available id that matches the
+ * pattern "prefix%d".
+ *
+ * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters
+ * in methods and we're not going to do a dedicated type, we just use an object array which
+ * must contain one initial item and several are built on the fly just for internal storage:
+ * <ul>
+ * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null.
+ * <li> index(Integer): The minimum index of the generated id. Must start with null.
+ * <li> generated(String): The generated widget currently being searched. Must start with null.
+ * <li> map(Set<String>): A set of the ids collected so far when walking through the widget
+ * hierarchy. Must start with null.
+ * </ul>
+ *
+ * @param uiRoot The Ui root node where to start searching recursively. For the initial call
+ * you want to pass the document root.
+ * @param params An in-out context of parameters used during recursion, as explained above.
+ * @return A suitable generated id
+ */
+ @SuppressWarnings("unchecked")
+ private static String getFreeWidgetId(UiElementNode uiRoot,
+ Object[] params) {
+
+ Set<String> map = (Set<String>)params[3];
+ if (map == null) {
+ params[3] = map = new HashSet<String>();
+ }
+
+ int num = params[1] == null ? 0 : ((Integer)params[1]).intValue();
+
+ String generated = (String) params[2];
+ String prefix = (String) params[0];
+ if (generated == null) {
+ int pos = prefix.indexOf('.');
+ if (pos >= 0) {
+ prefix = prefix.substring(pos + 1);
+ }
+ pos = prefix.indexOf('$');
+ if (pos >= 0) {
+ prefix = prefix.substring(pos + 1);
+ }
+ prefix = prefix.replaceAll("[^a-zA-Z]", ""); //$NON-NLS-1$ $NON-NLS-2$
+ if (prefix.length() == 0) {
+ prefix = DEFAULT_WIDGET_PREFIX;
+ } else {
+ // Lowercase initial character
+ prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1);
+ }
+
+ // Note that we perform locale-independent lowercase checks; in "Image" we
+ // want the lowercase version to be "image", not "?mage" where ? is
+ // the char LATIN SMALL LETTER DOTLESS I.
+ do {
+ num++;
+ generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
+ } while (map.contains(generated.toLowerCase(Locale.US)));
+
+ params[0] = prefix;
+ params[1] = num;
+ params[2] = generated;
+ }
+
+ String id = uiRoot.getAttributeValue(ATTR_ID);
+ if (id != null) {
+ id = id.replace(NEW_ID_PREFIX, ""); //$NON-NLS-1$
+ id = id.replace(ID_PREFIX, ""); //$NON-NLS-1$
+ if (map.add(id.toLowerCase(Locale.US))
+ && map.contains(generated.toLowerCase(Locale.US))) {
+
+ do {
+ num++;
+ generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
+ } while (map.contains(generated.toLowerCase(Locale.US)));
+
+ params[1] = num;
+ params[2] = generated;
+ }
+ }
+
+ for (UiElementNode uiChild : uiRoot.getUiChildren()) {
+ getFreeWidgetId(uiChild, params);
+ }
+
+ // Note: return params[2] (not "generated") since it could have changed during recursion.
+ return (String) params[2];
+ }
+
+ /**
+ * Returns true if the given descriptor represents a view that not only can have
+ * children but which allows us to <b>insert</b> children. Some views, such as
+ * ListView (and in general all AdapterViews), disallow children to be inserted except
+ * through the dedicated AdapterView interface to do it.
+ *
+ * @param descriptor the descriptor for the view in question
+ * @param viewObject an actual instance of the view, or null if not available
+ * @return true if the descriptor describes a view which allows insertion of child
+ * views
+ */
+ public static boolean canInsertChildren(ElementDescriptor descriptor, Object viewObject) {
+ if (descriptor.hasChildren()) {
+ if (viewObject != null) {
+ // We have a view object; see if it derives from an AdapterView
+ Class<?> clz = viewObject.getClass();
+ while (clz != null) {
+ if (clz.getName().equals(FQCN_ADAPTER_VIEW)) {
+ return false;
+ }
+ clz = clz.getSuperclass();
+ }
+ } else {
+ // No view object, so we can't easily look up the class and determine
+ // whether it's an AdapterView; instead, look at the fixed list of builtin
+ // concrete subclasses of AdapterView
+ String viewName = descriptor.getXmlLocalName();
+ if (viewName.equals(LIST_VIEW) || viewName.equals(EXPANDABLE_LIST_VIEW)
+ || viewName.equals(GALLERY) || viewName.equals(GRID_VIEW)) {
+
+ // We should really also enforce that
+ // XmlUtils.ANDROID_URI.equals(descriptor.getNameSpace())
+ // here and if not, return true, but it turns out the getNameSpace()
+ // for elements are often "".
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DocumentDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DocumentDescriptor.java
new file mode 100644
index 000000000..695327847
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DocumentDescriptor.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+/**
+ * {@link DocumentDescriptor} describes the properties expected for an XML document node.
+ *
+ * Compared to ElementDescriptor, {@link DocumentDescriptor} does not have XML name nor UI name,
+ * tooltip, SDK url and attributes list.
+ * <p/>
+ * It has a children list which represent all the possible roots of the document.
+ * <p/>
+ * The document nodes are "mandatory", meaning the UI node is never deleted and it may lack
+ * an actual XML node attached.
+ */
+public class DocumentDescriptor extends ElementDescriptor {
+
+ /**
+ * Constructs a new {@link DocumentDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ * <p/>
+ * The XML name is never shown in the UI directly. It is however used when an icon
+ * needs to be found for the node.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public DocumentDescriptor(String xml_name, ElementDescriptor[] children) {
+ super(xml_name, children, Mandatory.MANDATORY);
+ }
+
+ /**
+ * @return A new {@link UiElementNode} linked to this descriptor.
+ */
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiDocumentNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java
new file mode 100644
index 000000000..0d62ec00c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * {@link ElementDescriptor} describes the properties expected for a given XML element node.
+ *
+ * {@link ElementDescriptor} have an XML name, UI name, a tooltip, an SDK url,
+ * an attributes list and a children list.
+ *
+ * An UI node can be "mandatory", meaning the UI node is never deleted and it may lack
+ * an actual XML node attached. A non-mandatory UI node MUST have an XML node attached
+ * and it will cease to exist when the XML node ceases to exist.
+ */
+public class ElementDescriptor implements Comparable<ElementDescriptor> {
+ private static final String ELEMENT_ICON_FILENAME = "element"; //$NON-NLS-1$
+
+ /** The XML element node name. Case sensitive. */
+ protected final String mXmlName;
+ /** The XML element name for the user interface, typically capitalized. */
+ private final String mUiName;
+ /** The list of allowed attributes. */
+ private AttributeDescriptor[] mAttributes;
+ /** The list of allowed children */
+ private ElementDescriptor[] mChildren;
+ /* An optional tooltip. Can be empty. */
+ private String mTooltip;
+ /** An optional SKD URL. Can be empty. */
+ private String mSdkUrl;
+ /** Whether this UI node must always exist (even for empty models). */
+ private final Mandatory mMandatory;
+
+ public enum Mandatory {
+ NOT_MANDATORY,
+ MANDATORY,
+ MANDATORY_LAST
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name, UI name,
+ * tooltip, SDK url, attributes list, children list and mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children,
+ Mandatory mandatory) {
+ mMandatory = mandatory;
+ mXmlName = xml_name;
+ mUiName = ui_name;
+ mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null;
+ mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null;
+ setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{});
+ mChildren = children != null ? children : new ElementDescriptor[]{};
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name, UI name,
+ * tooltip, SDK url, attributes list, children list and mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children,
+ boolean mandatory) {
+ mMandatory = mandatory ? Mandatory.MANDATORY : Mandatory.NOT_MANDATORY;
+ mXmlName = xml_name;
+ mUiName = ui_name;
+ mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null;
+ mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null;
+ setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{});
+ mChildren = children != null ? children : new ElementDescriptor[]{};
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ElementDescriptor(String xml_name, ElementDescriptor[] children, Mandatory mandatory) {
+ this(xml_name, prettyName(xml_name), null, null, null, children, mandatory);
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public ElementDescriptor(String xml_name, ElementDescriptor[] children) {
+ this(xml_name, prettyName(xml_name), null, null, null, children, false);
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ */
+ public ElementDescriptor(String xml_name) {
+ this(xml_name, prettyName(xml_name), null, null, null, null, false);
+ }
+
+ /** Returns whether this node must always exist (even for empty models) */
+ public Mandatory getMandatory() {
+ return mMandatory;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s [%s, attr %d, children %d%s]", //$NON-NLS-1$
+ this.getClass().getSimpleName(),
+ mXmlName,
+ mAttributes != null ? mAttributes.length : 0,
+ mChildren != null ? mChildren.length : 0,
+ mMandatory != Mandatory.NOT_MANDATORY ? ", " + mMandatory.toString() : "" //$NON-NLS-1$ //$NON-NLS-2$
+ );
+ }
+
+ /**
+ * Returns the XML element node local name (case sensitive)
+ */
+ public final String getXmlLocalName() {
+ int pos = mXmlName.indexOf(':');
+ if (pos != -1) {
+ return mXmlName.substring(pos+1);
+ }
+ return mXmlName;
+ }
+
+ /**
+ * Returns the XML element node name, including the prefix.
+ * Case sensitive.
+ * <p/>
+ * In Android resources, the element node name for Android resources typically does not
+ * have a prefix and is typically the simple Java class name (e.g. "View"), whereas for
+ * custom views it is generally the fully qualified class name of the view (e.g.
+ * "com.mycompany.myapp.MyView").
+ * <p/>
+ * Most of the time you'll probably want to use {@link #getXmlLocalName()} to get a local
+ * name guaranteed without a prefix.
+ * <p/>
+ * Note that the prefix that <em>may</em> be available in this descriptor has nothing to
+ * do with the actual prefix the node might have (or needs to have) in the actual XML file
+ * since descriptors are fixed and do not depend on any current namespace defined in the
+ * target XML.
+ */
+ public String getXmlName() {
+ return mXmlName;
+ }
+
+ /**
+ * Returns the namespace of the attribute.
+ */
+ public final String getNamespace() {
+ // For now we hard-code the prefix as being "android"
+ if (mXmlName.startsWith(ANDROID_NS_NAME_PREFIX)) {
+ return ANDROID_URI;
+ }
+
+ return ""; //$NON-NLs-1$
+ }
+
+
+ /** Returns the XML element name for the user interface, typically capitalized. */
+ public String getUiName() {
+ return mUiName;
+ }
+
+ /**
+ * Returns an icon for the element.
+ * This icon is generic, that is all element descriptors have the same icon
+ * no matter what they represent.
+ *
+ * @return An icon for this element or null.
+ * @see #getCustomizedIcon()
+ */
+ public Image getGenericIcon() {
+ return IconFactory.getInstance().getIcon(ELEMENT_ICON_FILENAME);
+ }
+
+ /**
+ * Returns an optional icon for the element, typically to be used in XML form trees.
+ * <p/>
+ * This icon is customized to the given descriptor, that is different elements
+ * will have different icons.
+ * <p/>
+ * By default this tries to return an icon based on the XML name of the element.
+ * If this fails, it tries to return the default Android logo as defined in the
+ * plugin. If all fails, it returns null.
+ *
+ * @return An icon for this element. This is never null.
+ */
+ public Image getCustomizedIcon() {
+ IconFactory factory = IconFactory.getInstance();
+ int color = hasChildren() ? IconFactory.COLOR_BLUE
+ : IconFactory.COLOR_GREEN;
+ int shape = hasChildren() ? IconFactory.SHAPE_RECT
+ : IconFactory.SHAPE_CIRCLE;
+ String name = mXmlName;
+
+ int pos = name.lastIndexOf('.');
+ if (pos != -1) {
+ // If the user uses a fully qualified name, such as
+ // "android.gesture.GestureOverlayView" in their XML, we need to
+ // look up only by basename
+ name = name.substring(pos + 1);
+ }
+ Image icon = factory.getIcon(name, color, shape);
+ if (icon == null) {
+ icon = getGenericIcon();
+ }
+ if (icon == null) {
+ icon = AdtPlugin.getAndroidLogo();
+ }
+ return icon;
+ }
+
+ /**
+ * Returns an optional ImageDescriptor for the element.
+ * <p/>
+ * By default this tries to return an image based on the XML name of the element.
+ * If this fails, it tries to return the default Android logo as defined in the
+ * plugin. If all fails, it returns null.
+ *
+ * @return An ImageDescriptor for this element or null.
+ */
+ public ImageDescriptor getImageDescriptor() {
+ IconFactory factory = IconFactory.getInstance();
+ int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN;
+ int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE;
+ ImageDescriptor id = factory.getImageDescriptor(mXmlName, color, shape);
+ return id != null ? id : AdtPlugin.getAndroidLogoDesc();
+ }
+
+ /* Returns the list of allowed attributes. */
+ public AttributeDescriptor[] getAttributes() {
+ return mAttributes;
+ }
+
+ /** Sets the list of allowed attributes. */
+ public void setAttributes(AttributeDescriptor[] attributes) {
+ mAttributes = attributes;
+ for (AttributeDescriptor attribute : attributes) {
+ attribute.setParent(this);
+ }
+ }
+
+ /** Returns the list of allowed children */
+ public ElementDescriptor[] getChildren() {
+ return mChildren;
+ }
+
+ /** @return True if this descriptor has children available */
+ public boolean hasChildren() {
+ return mChildren.length > 0;
+ }
+
+ /**
+ * Checks whether this descriptor can accept the given descriptor type
+ * as a direct child.
+ *
+ * @return True if this descriptor can accept children of the given descriptor type.
+ * False if not accepted, no children allowed, or target is null.
+ */
+ public boolean acceptChild(ElementDescriptor target) {
+ if (target != null && mChildren.length > 0) {
+ String targetXmlName = target.getXmlName();
+ for (ElementDescriptor child : mChildren) {
+ if (child.getXmlName().equals(targetXmlName)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /** Sets the list of allowed children. */
+ public void setChildren(ElementDescriptor[] newChildren) {
+ mChildren = newChildren;
+ }
+
+ /**
+ * Sets the list of allowed children.
+ * <p/>
+ * This is just a convenience method that converts a Collection into an array and
+ * calls {@link #setChildren(ElementDescriptor[])}.
+ * <p/>
+ * This means a <em>copy</em> of the collection is made. The collection is not
+ * stored by the recipient and can thus be altered by the caller.
+ */
+ public void setChildren(Collection<ElementDescriptor> newChildren) {
+ setChildren(newChildren.toArray(new ElementDescriptor[newChildren.size()]));
+ }
+
+ /**
+ * Returns an optional tooltip. Will be null if not present.
+ * <p/>
+ * The tooltip is based on the Javadoc of the element and already processed via
+ * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as
+ * a UI tooltip.
+ */
+ public String getTooltip() {
+ return mTooltip;
+ }
+
+ /** Returns an optional SKD URL. Will be null if not present. */
+ public String getSdkUrl() {
+ return mSdkUrl;
+ }
+
+ /** Sets the optional tooltip. Can be null or empty. */
+ public void setTooltip(String tooltip) {
+ mTooltip = tooltip;
+ }
+
+ /** Sets the optional SDK URL. Can be null or empty. */
+ public void setSdkUrl(String sdkUrl) {
+ mSdkUrl = sdkUrl;
+ }
+
+ /**
+ * @return A new {@link UiElementNode} linked to this descriptor.
+ */
+ public UiElementNode createUiNode() {
+ return new UiElementNode(this);
+ }
+
+ /**
+ * Returns the first children of this descriptor that describes the given XML element name.
+ * <p/>
+ * In recursive mode, searches the direct children first before descending in the hierarchy.
+ *
+ * @return The ElementDescriptor matching the requested XML node element name or null.
+ */
+ public ElementDescriptor findChildrenDescriptor(String element_name, boolean recursive) {
+ return findChildrenDescriptorInternal(element_name, recursive, null);
+ }
+
+ private ElementDescriptor findChildrenDescriptorInternal(String element_name,
+ boolean recursive,
+ Set<ElementDescriptor> visited) {
+ if (recursive && visited == null) {
+ visited = new HashSet<ElementDescriptor>();
+ }
+
+ for (ElementDescriptor e : getChildren()) {
+ if (e.getXmlName().equals(element_name)) {
+ return e;
+ }
+ }
+
+ if (visited != null) {
+ visited.add(this);
+ }
+
+ if (recursive) {
+ for (ElementDescriptor e : getChildren()) {
+ if (visited != null) {
+ if (!visited.add(e)) { // Set.add() returns false if element is already present
+ continue;
+ }
+ }
+ ElementDescriptor f = e.findChildrenDescriptorInternal(element_name,
+ recursive, visited);
+ if (f != null) {
+ return f;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility helper than pretty-formats an XML Name for the UI.
+ * This is used by the simplified constructor that takes only an XML element name.
+ *
+ * @param xml_name The XML name to convert.
+ * @return The XML name with dashes replaced by spaces and capitalized.
+ */
+ private static String prettyName(String xml_name) {
+ char c[] = xml_name.toCharArray();
+ if (c.length > 0) {
+ c[0] = Character.toUpperCase(c[0]);
+ }
+ return new String(c).replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * Returns true if this node defines the given attribute
+ *
+ * @param namespaceUri the namespace URI of the target attribute
+ * @param attributeName the attribute name
+ * @return true if this element defines an attribute of the given name and namespace
+ */
+ public boolean definesAttribute(String namespaceUri, String attributeName) {
+ for (AttributeDescriptor desc : mAttributes) {
+ if (desc.getXmlLocalName().equals(attributeName) &&
+ desc.getNamespaceUri().equals(namespaceUri)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Implements Comparable<ElementDescriptor>:
+ @Override
+ public int compareTo(ElementDescriptor o) {
+ return mUiName.compareToIgnoreCase(o.mUiName);
+ }
+
+ /**
+ * Ensures that this view descriptor's attribute list is up to date. This is
+ * always the case for all the builtin descriptors, but for example for a
+ * custom view, it could be changing dynamically so caches may have to be
+ * recomputed. This method will return true if nothing changed, and false if
+ * it recomputed its info.
+ *
+ * @return true if the attributes are already up to date and nothing changed
+ */
+ public boolean syncAttributes() {
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/EnumAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/EnumAttributeDescriptor.java
new file mode 100644
index 000000000..29233571b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/EnumAttributeDescriptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiListAttributeNode;
+
+/**
+ * Describes a text attribute that can only contains some predefined values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ */
+public class EnumAttributeDescriptor extends ListAttributeDescriptor {
+
+ public EnumAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ }
+
+ /**
+ * @return A new {@link UiListAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiListAttributeNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/FlagAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/FlagAttributeDescriptor.java
new file mode 100644
index 000000000..4f4b21569
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/FlagAttributeDescriptor.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.ui.FlagValueCellEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Describes a text attribute that can only contains some predefined values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ *
+ * Note: in Android resources, a "flag" is a list of fixed values where one or
+ * more values can be selected using an "or", e.g. "align='left|top'".
+ * By contrast, an "enum" is a list of fixed values of which only one can be
+ * selected at a given time, e.g. "gravity='right'".
+ * <p/>
+ * This class handles the "flag" case.
+ * The "enum" case is done using {@link ListAttributeDescriptor}.
+ */
+public class FlagAttributeDescriptor extends TextAttributeDescriptor {
+
+ private String[] mNames;
+
+ /**
+ * Creates a new {@link FlagAttributeDescriptor}.
+ * <p/>
+ * If <code>attrInfo</code> is not null and has non-null flag values, these will be
+ * used for the list.
+ * Otherwise values are automatically extracted from the FrameworkResourceManager.
+ */
+ public FlagAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ if (attrInfo != null) {
+ mNames = attrInfo.getFlagValues();
+ }
+ }
+
+ /**
+ * Creates a new {@link FlagAttributeDescriptor} which uses the provided values
+ * and does not lookup the content of <code>attrInfo</code>.
+ */
+ public FlagAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip, IAttributeInfo attrInfo, String[] names) {
+ super(xmlLocalName, nsUri, attrInfo);
+ mNames = names;
+ }
+
+ /**
+ * @return The initial names of the flags. Can be null, in which case the Framework
+ * resource parser should be checked.
+ */
+ public String[] getNames() {
+ return mNames;
+ }
+
+ /**
+ * @return A new {@link UiListAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiFlagAttributeNode(this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new FlagValueCellEditor(parent);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IDescriptorProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IDescriptorProvider.java
new file mode 100644
index 000000000..860ed394e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IDescriptorProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+public interface IDescriptorProvider {
+
+ ElementDescriptor[] getRootElementDescriptors();
+
+ ElementDescriptor getDescriptor();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ITextAttributeCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ITextAttributeCreator.java
new file mode 100755
index 000000000..1fc662364
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ITextAttributeCreator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.IAttributeInfo;
+
+
+/**
+ * The {@link ITextAttributeCreator} interface is used by the appendAttribute(...) in
+ * {@link DescriptorsUtils} to allows callers to override the kind of
+ * {@link TextAttributeDescriptor} created for a given XML attribute name.
+ * <p/>
+ * The <code>create()</code> method must take arguments that are similar to the
+ * single constructor for {@link TextAttributeDescriptor}.
+ */
+public interface ITextAttributeCreator {
+
+ /**
+ * Creates a new {@link TextAttributeDescriptor} instance for the given XML name,
+ * UI name and tooltip.
+ *
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null.
+ * @return A new {@link TextAttributeDescriptor} (or derived) instance.
+ */
+ public TextAttributeDescriptor create(
+ String xmlLocalName,
+ String nsUri,
+ IAttributeInfo attrInfo);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IUnknownDescriptorProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IUnknownDescriptorProvider.java
new file mode 100755
index 000000000..931c1b726
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IUnknownDescriptorProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.values.uimodel.UiItemElementNode;
+
+/**
+ * {@link UiItemElementNode} is the main class that creates the UI Model hierarchy based
+ * on an XML DOM hierarchy, matching XML names to the {@link ElementDescriptor} names.
+ * <p/>
+ * This interface declares a provider that can provide an {@link ElementDescriptor}
+ * for an unknown XML local name.
+ */
+public interface IUnknownDescriptorProvider {
+
+ /**
+ * Returns an instance of {@link ElementDescriptor} matching the given XML Local Name.
+ *
+ * @param xmlLocalName The XML local name.
+ * @return A new or existing {@link ElementDescriptor} or derived instance. Must not be null.
+ */
+ ElementDescriptor getDescriptor(String xmlLocalName);
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ListAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ListAttributeDescriptor.java
new file mode 100644
index 000000000..16b0d55f1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ListAttributeDescriptor.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.ui.ListValueCellEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Describes a text attribute that can contains some predefined values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ */
+public class ListAttributeDescriptor extends TextAttributeDescriptor {
+
+ private String[] mValues = null;
+
+ /**
+ * Used by {@link DescriptorsUtils} to create instances of this descriptor.
+ */
+ public static final ITextAttributeCreator CREATOR = new ITextAttributeCreator() {
+ @Override
+ public TextAttributeDescriptor create(String xmlLocalName,
+ String nsUri, IAttributeInfo attrInfo) {
+ return new ListAttributeDescriptor(xmlLocalName, nsUri, attrInfo);
+ }
+ };
+
+ /**
+ * Creates a new {@link ListAttributeDescriptor}.
+ * <p/>
+ * If <code>attrInfo</code> is not null and has non-null enum values, these will be
+ * used for the list.
+ * Otherwise values are automatically extracted from the FrameworkResourceManager.
+ */
+ public ListAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ if (attrInfo != null) {
+ mValues = attrInfo.getEnumValues();
+ }
+ }
+
+ /**
+ * Creates a new {@link ListAttributeDescriptor} which uses the provided values
+ * and does not lookup the content of <code>attrInfo</code>.
+ */
+ public ListAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo,
+ String[] values) {
+ super(xmlLocalName, nsUri, attrInfo);
+ mValues = values;
+ }
+
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /**
+ * @return A new {@link UiListAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiListAttributeNode(this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new ListValueCellEditor(parent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java
new file mode 100644
index 000000000..0f146c198
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.ui.ResourceValueCellEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+import com.android.resources.ResourceType;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Describes an XML attribute displayed containing a value or a reference to a resource.
+ * It is displayed by a {@link UiResourceAttributeNode}.
+ */
+public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor {
+
+ /**
+ * The {@link ResourceType} that this reference attribute can accept. It can be null,
+ * in which case any reference type can be used.
+ */
+ private ResourceType mResourceType;
+
+ /**
+ * Used by {@link DescriptorsUtils} to create instances of this descriptor.
+ */
+ public static final ITextAttributeCreator CREATOR = new ITextAttributeCreator() {
+ @Override
+ public TextAttributeDescriptor create(String xmlLocalName,
+ String nsUri, IAttributeInfo attrInfo) {
+ return new ReferenceAttributeDescriptor(
+ ResourceType.DRAWABLE,
+ xmlLocalName, nsUri,
+ new AttributeInfo(xmlLocalName, Format.REFERENCE_SET));
+ }
+ };
+
+ /**
+ * Creates a reference attributes that can contain any type of resources.
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null.
+ */
+ public ReferenceAttributeDescriptor(String xmlLocalName, String nsUri,
+ IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ }
+
+ /**
+ * Creates a reference attributes that can contain a reference to a specific
+ * {@link ResourceType}.
+ * @param resourceType The specific {@link ResourceType} that this reference attribute supports.
+ * It can be <code>null</code>, in which case, all resource types are supported.
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null.
+ */
+ public ReferenceAttributeDescriptor(ResourceType resourceType,
+ String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ mResourceType = resourceType;
+ }
+
+
+ /** Returns the {@link ResourceType} that this reference attribute can accept.
+ * It can be null, in which case any reference type can be used. */
+ public ResourceType getResourceType() {
+ return mResourceType;
+ }
+
+ /**
+ * @return A new {@link UiResourceAttributeNode} linked to this reference descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiResourceAttributeNode(mResourceType, this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new ResourceValueCellEditor(parent);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/SeparatorAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/SeparatorAttributeDescriptor.java
new file mode 100644
index 000000000..034bf8eb0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/SeparatorAttributeDescriptor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiSeparatorAttributeNode;
+
+/**
+ * {@link SeparatorAttributeDescriptor} does not represent any real attribute.
+ * <p/>
+ * It is used to separate groups of attributes visually.
+ */
+public class SeparatorAttributeDescriptor extends AttributeDescriptor {
+
+ /**
+ * Creates a new {@link SeparatorAttributeDescriptor}
+ */
+ public SeparatorAttributeDescriptor(String label) {
+ super(label /* xmlLocalName */, null /* nsUri */, null /* info */);
+ }
+
+ /**
+ * @return A new {@link UiAttributeNode} linked to this descriptor or null if this
+ * attribute has no user interface.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiSeparatorAttributeNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextAttributeDescriptor.java
new file mode 100644
index 000000000..f8c7806ae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextAttributeDescriptor.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.eclipse.adt.internal.editors.ui.TextValueCellEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+
+import java.util.EnumSet;
+import java.util.Locale;
+
+
+/**
+ * Describes a textual XML attribute.
+ * <p/>
+ * Such an attribute has a tooltip and would typically be displayed by
+ * {@link UiTextAttributeNode} using a label widget and text field.
+ * <p/>
+ * This is the "default" kind of attribute. If in doubt, use this.
+ */
+public class TextAttributeDescriptor extends AttributeDescriptor implements IPropertyDescriptor {
+ public static final String DEPRECATED_CATEGORY = "Deprecated";
+
+ private String mUiName;
+ private String mTooltip;
+ private boolean mRequired;
+
+ /**
+ * Creates a new {@link TextAttributeDescriptor}
+ *
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null.
+ */
+ public TextAttributeDescriptor(
+ String xmlLocalName,
+ String nsUri,
+ IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ }
+
+ /**
+ * @return The UI name of the attribute. Cannot be an empty string and cannot be null.
+ */
+ @NonNull
+ public String getUiName() {
+ if (mUiName == null) {
+ IAttributeInfo info = getAttributeInfo();
+ if (info != null) {
+ mUiName = DescriptorsUtils.prettyAttributeUiName(info.getName());
+ if (mRequired) {
+ mUiName += "*"; //$NON-NLS-1$
+ }
+ } else {
+ mUiName = getXmlLocalName();
+ }
+ }
+
+ return mUiName;
+ }
+
+
+ /**
+ * Sets the UI name to be associated with this descriptor. This is usually
+ * computed lazily from the {@link #getAttributeInfo()} data, but for some
+ * hardcoded/builtin descriptor this is manually initialized.
+ *
+ * @param uiName the new UI name to be used
+ * @return this, for constructor setter chaining
+ */
+ public TextAttributeDescriptor setUiName(String uiName) {
+ mUiName = uiName;
+
+ return this;
+ }
+
+ /**
+ * Sets the tooltip to be associated with this descriptor. This is usually
+ * computed lazily from the {@link #getAttributeInfo()} data, but for some
+ * hardcoded/builtin descriptor this is manually initialized.
+ *
+ * @param tooltip the new tooltip to be used
+ * @return this, for constructor setter chaining
+ */
+ public TextAttributeDescriptor setTooltip(String tooltip) {
+ mTooltip = tooltip;
+
+ return this;
+ }
+
+ /**
+ * Sets whether this attribute is required
+ *
+ * @param required whether this attribute is required
+ * @return this, for constructor setter chaining
+ */
+ public TextAttributeDescriptor setRequired(boolean required) {
+ mRequired = required;
+
+ return this;
+ }
+
+ /**
+ * Returns whether this attribute is required
+ *
+ * @return whether this attribute is required
+ */
+ public boolean isRequired() {
+ return mRequired;
+ }
+
+ /**
+ * The tooltip string is either null or a non-empty string.
+ * <p/>
+ * The tooltip is based on the Javadoc of the attribute and already processed via
+ * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as
+ * a UI tooltip.
+ * <p/>
+ * An empty string is converted to null, to match the behavior of setToolTipText() in
+ * {@link Control}.
+ *
+ * @return A non-empty tooltip string or null
+ */
+ @Nullable
+ public String getTooltip() {
+ if (mTooltip == null) {
+ IAttributeInfo info = getAttributeInfo();
+ if (info == null) {
+ mTooltip = "";
+ return mTooltip;
+ }
+
+ String tooltip = null;
+ String rawTooltip = info.getJavaDoc();
+ if (rawTooltip == null) {
+ rawTooltip = "";
+ }
+
+ String deprecated = info.getDeprecatedDoc();
+ if (deprecated != null) {
+ if (rawTooltip.length() > 0) {
+ rawTooltip += "@@"; //$NON-NLS-1$ insert a break
+ }
+ rawTooltip += "* Deprecated";
+ if (deprecated.length() != 0) {
+ rawTooltip += ": " + deprecated; //$NON-NLS-1$
+ }
+ if (deprecated.length() == 0 || !deprecated.endsWith(".")) { //$NON-NLS-1$
+ rawTooltip += "."; //$NON-NLS-1$
+ }
+ }
+
+ // Add the known types to the tooltip
+ EnumSet<Format> formats_list = info.getFormats();
+ int flen = formats_list.size();
+ if (flen > 0) {
+ StringBuilder sb = new StringBuilder();
+ if (rawTooltip != null && rawTooltip.length() > 0) {
+ sb.append(rawTooltip);
+ sb.append(" "); //$NON-NLS-1$
+ }
+ if (sb.length() > 0) {
+ sb.append("@@"); //$NON-NLS-1$ @@ inserts a break before the types
+ }
+ sb.append("["); //$NON-NLS-1$
+ boolean isFirst = true;
+ for (Format f : formats_list) {
+ if (isFirst) {
+ isFirst = false;
+ } else {
+ sb.append(", ");
+ }
+ sb.append(f.toString().toLowerCase(Locale.US));
+ }
+ // The extra space at the end makes the tooltip more readable on Windows.
+ sb.append("]"); //$NON-NLS-1$
+
+ if (mRequired) {
+ // Note: this string is split in 2 to make it translatable.
+ sb.append(".@@"); //$NON-NLS-1$ @@ inserts a break and is not translatable
+ sb.append("* Required.");
+ }
+
+ // The extra space at the end makes the tooltip more readable on Windows.
+ sb.append(" "); //$NON-NLS-1$
+
+ rawTooltip = sb.toString();
+ tooltip = DescriptorsUtils.formatTooltip(rawTooltip);
+ }
+
+ if (tooltip == null) {
+ tooltip = DescriptorsUtils.formatTooltip(rawTooltip);
+ }
+ mTooltip = tooltip;
+ }
+
+ return mTooltip.isEmpty() ? null : mTooltip;
+ }
+
+ /**
+ * @return A new {@link UiTextAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiTextAttributeNode(this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new TextValueCellEditor(parent);
+ }
+
+ @Override
+ public String getCategory() {
+ if (isDeprecated()) {
+ return DEPRECATED_CATEGORY;
+ }
+
+ ElementDescriptor parent = getParent();
+ if (parent != null) {
+ return parent.getUiName();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getDescription() {
+ return getTooltip();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return getUiName();
+ }
+
+ @Override
+ public String[] getFilterFlags() {
+ return null;
+ }
+
+ @Override
+ public Object getHelpContextIds() {
+ return null;
+ }
+
+ @Override
+ public Object getId() {
+ return this;
+ }
+
+ @Override
+ public ILabelProvider getLabelProvider() {
+ return AttributeDescriptorLabelProvider.getProvider();
+ }
+
+ @Override
+ public boolean isCompatibleWith(IPropertyDescriptor anotherProperty) {
+ return anotherProperty == this;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextValueDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextValueDescriptor.java
new file mode 100644
index 000000000..6bfe4c778
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextValueDescriptor.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextValueNode;
+
+
+/**
+ * Describes the value of an XML element.
+ * <p/>
+ * The value is a simple text string, displayed by an {@link UiTextValueNode}.
+ */
+public class TextValueDescriptor extends TextAttributeDescriptor {
+
+ /**
+ * Creates a new {@link TextValueDescriptor}
+ *
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param tooltip A non-empty tooltip string or null
+ */
+ public TextValueDescriptor(String uiName, String tooltip) {
+ super("#text" /* xmlLocalName */, null /* nsUri */, null /* info */);
+ setUiName(uiName);
+ setTooltip(tooltip);
+ }
+
+ /**
+ * @return A new {@link UiTextValueNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiTextValueNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/XmlnsAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/XmlnsAttributeDescriptor.java
new file mode 100644
index 000000000..39bb0f5f8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/XmlnsAttributeDescriptor.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import static com.android.SdkConstants.XMLNS;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+
+/**
+ * Describes an XMLNS attribute that is hidden.
+ * <p/>
+ * Such an attribute has no user interface and no corresponding {@link UiAttributeNode}.
+ * It also has a single constant default value.
+ * <p/>
+ * When loading an XML, we'll ignore this attribute.
+ * However when writing a new XML, we should always write this attribute.
+ * <p/>
+ * Currently this is used for the xmlns:android attribute in the manifest element.
+ */
+public final class XmlnsAttributeDescriptor extends AttributeDescriptor {
+
+ private String mValue;
+
+ public XmlnsAttributeDescriptor(String defaultPrefix, String value) {
+ super(defaultPrefix, XMLNS_URI, null /* info */);
+ mValue = value;
+ }
+
+ /**
+ * Returns the value of this specialized attribute descriptor, which is the URI associated
+ * to the declared namespace prefix.
+ */
+ public String getValue() {
+ return mValue;
+ }
+
+ /**
+ * Returns the "xmlns" prefix that is always used by this node for its namespace URI.
+ * This is defined by the XML specification.
+ */
+ public String getXmlNsPrefix() {
+ return XMLNS;
+ }
+
+ /**
+ * Returns the fully-qualified attribute name, namely "xmlns:xxx" where xxx is
+ * the defaultPrefix passed in the constructor.
+ */
+ public String getXmlNsName() {
+ return getXmlNsPrefix() + ":" + getXmlLocalName(); //$NON-NLS-1$
+ }
+
+ /**
+ * @return Always returns null. {@link XmlnsAttributeDescriptor} has no user interface.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java
new file mode 100644
index 000000000..48ef7c366
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch;
+
+import static com.android.SdkConstants.DOT_9PNG;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.ui.ImageViewer;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.ui.MainFrame;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.dialogs.SaveAsDialog;
+import org.eclipse.ui.part.EditorPart;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+
+/**
+ * Draw9Patch editor part.
+ */
+public class Draw9PatchEditor extends EditorPart implements ImageViewer.UpdateListener {
+
+ private IProject mProject = null;
+
+ private FileEditorInput mFileEditorInput = null;
+
+ private String mFileName = null;
+
+ private NinePatchedImage mNinePatchedImage = null;
+
+ private MainFrame mMainFrame = null;
+
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ setSite(site);
+ setInput(input);
+ setPartName(input.getName());
+
+ // The contract of init() mentions we need to fail if we can't
+ // understand the input.
+ if (input instanceof FileEditorInput) {
+ // We try to open a file that is part of the current workspace
+ mFileEditorInput = (FileEditorInput) input;
+ mFileName = mFileEditorInput.getName();
+ mProject = mFileEditorInput.getFile().getProject();
+ } else {
+ throw new PartInitException("Input is not of type FileEditorInput " + //$NON-NLS-1$
+ "nor FileStoreEditorInput: " + //$NON-NLS-1$
+ input == null ? "null" : input.toString()); //$NON-NLS-1$
+ }
+
+ }
+
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ @Override
+ public void doSaveAs() {
+ IPath relativePath = null;
+ if ((relativePath = showSaveAsDialog()) != null) {
+ mFileEditorInput = new FileEditorInput(ResourcesPlugin.getWorkspace().getRoot()
+ .getFile(relativePath));
+ mFileName = mFileEditorInput.getName();
+ setInput(mFileEditorInput);
+
+ doSave(new NullProgressMonitor());
+ }
+ }
+
+ @Override
+ public void doSave(final IProgressMonitor monitor) {
+ boolean hasNinePatchExtension = mFileName.endsWith(DOT_9PNG);
+ boolean doConvert = false;
+
+ if (!hasNinePatchExtension) {
+ String patchedName = NinePatchedImage.getNinePatchedFileName(mFileName);
+ doConvert = MessageDialog
+ .openQuestion(AdtPlugin.getDisplay().getActiveShell(),
+ "Warning",
+ String.format(
+ "The file \"%s\" doesn't seem to be a 9-patch file. \n"
+ + "Do you want to convert and save as \"%s\" ?",
+ mFileName, patchedName));
+
+ if (doConvert) {
+ IFile destFile = mProject.getFile(NinePatchedImage.getNinePatchedFileName(
+ mFileEditorInput.getFile().getProjectRelativePath().toOSString()));
+ if (!destFile.exists()) {
+ mFileEditorInput = new FileEditorInput(destFile);
+ mFileName = mFileEditorInput.getName();
+ } else {
+ IPath relativePath = null;
+ if ((relativePath = showSaveAsDialog()) != null) {
+ mFileEditorInput = new FileEditorInput(ResourcesPlugin.getWorkspace()
+ .getRoot().getFile(relativePath));
+ mFileName = mFileEditorInput.getName();
+ } else {
+ doConvert = false;
+ }
+ }
+ }
+ }
+
+ if (hasNinePatchExtension || doConvert) {
+ ImageLoader loader = new ImageLoader();
+ loader.data = new ImageData[] {
+ mNinePatchedImage.getRawImageData()
+ };
+
+ IFile file = mFileEditorInput.getFile();
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ loader.save(outputStream, SWT.IMAGE_PNG);
+ byte[] byteArray = outputStream.toByteArray();
+
+ try {
+ if (file.exists()) {
+ file.setContents(new ByteArrayInputStream(byteArray), true, false, monitor);
+ } else {
+ file.create(new ByteArrayInputStream(byteArray), true, monitor);
+ }
+
+ mNinePatchedImage.clearDirtyFlag();
+
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ setPartName(mFileName);
+ firePropertyChange(PROP_DIRTY);
+ }
+ });
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mMainFrame = new MainFrame(parent, SWT.NULL);
+
+ ImageViewer imageViewer = mMainFrame.getImageEditorPanel().getImageViewer();
+ imageViewer.addUpdateListener(this);
+
+ mNinePatchedImage = imageViewer.loadFile(mFileEditorInput.getPath().toOSString());
+ if (mNinePatchedImage.hasNinePatchExtension()) {
+ if (!mNinePatchedImage.ensure9Patch() && showConvertMessageBox(mFileName)) {
+ // Reload image
+ mNinePatchedImage = imageViewer.loadFile(mFileEditorInput.getPath().toOSString());
+ mNinePatchedImage.convertToNinePatch();
+ }
+ } else {
+ mNinePatchedImage.convertToNinePatch();
+ }
+
+ imageViewer.startDisplay();
+
+ parent.layout();
+ }
+
+ @Override
+ public void setFocus() {
+ mMainFrame.forceFocus();
+ }
+
+ @Override
+ public boolean isDirty() {
+ return mNinePatchedImage.isDirty();
+ }
+
+ @Override
+ public void update(NinePatchedImage image) {
+ if (image.isDirty()) {
+ firePropertyChange(PROP_DIRTY);
+ }
+ }
+
+ private IPath showSaveAsDialog() {
+ SaveAsDialog dialog = new SaveAsDialog(AdtPlugin.getDisplay().getActiveShell());
+
+ IFile dest = mProject.getFile(NinePatchedImage.getNinePatchedFileName(
+ mFileEditorInput.getFile().getProjectRelativePath().toOSString()));
+ dialog.setOriginalFile(dest);
+
+ dialog.create();
+
+ if (dialog.open() == Window.CANCEL) {
+ return null;
+ }
+
+ return dialog.getResult();
+ }
+
+ private static boolean showConvertMessageBox(String fileName) {
+ return MessageDialog.openQuestion(
+ AdtPlugin.getDisplay().getActiveShell(),
+ "Warning",
+ String.format("The file \"%s\" doesn't seem to be a 9-patch file. \n"
+ + "Do you want to convert?", fileName));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java
new file mode 100644
index 000000000..74c2f043e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics;
+
+import org.eclipse.swt.graphics.ImageData;
+
+/**
+ * The utility class for SWT Image and ImageData manipulation.
+ */
+public class GraphicsUtilities {
+
+ /**
+ * Convert normal image to 9-patched.
+ * @return Returns 9-patched ImageData object. If image is null, returns null.
+ */
+ public static ImageData convertToNinePatch(ImageData image) {
+ if (image == null) {
+ return null;
+ }
+ ImageData result = new ImageData(image.width + 2, image.height + 2, image.depth,
+ image.palette);
+
+ final int[] colors = new int[image.width];
+ final byte[] alpha = new byte[image.width];
+
+ for (int y = 0; y < image.height; y++) {
+
+ // Copy pixels
+ image.getPixels(0, y, image.width, colors, 0);
+ result.setPixels(1, y + 1, image.width, colors, 0);
+
+ // Copy alpha
+ image.getAlphas(0, y, image.width, alpha, 0);
+ result.setAlphas(1, y + 1, image.width, alpha, 0);
+ }
+
+ return result;
+ }
+
+ /**
+ * Wipe all color and alpha pixels.
+ */
+ public static void clearImageData(ImageData imageData) {
+ if (imageData == null) {
+ throw new IllegalArgumentException("image data must not be null");
+ }
+ int width = imageData.width;
+ int height = imageData.height;
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ imageData.setPixel(x, y, 0x00000000);
+ imageData.setAlpha(x, y, 0x00);
+ }
+ }
+ }
+
+ /**
+ * Duplicate the image data.
+ * @return If image is null, return null.
+ */
+ public static ImageData copy(ImageData image) {
+ if (image == null) {
+ return null;
+ }
+ ImageData result = new ImageData(image.width, image.height, image.depth,
+ image.palette);
+
+ final int[] colors = new int[image.width];
+ final byte[] alpha = new byte[image.width];
+
+ for (int y = 0; y < image.height; y++) {
+
+ // Copy pixels
+ image.getPixels(0, y, image.width, colors, 0);
+ result.setPixels(0, y, image.width, colors, 0);
+
+ // Copy alpha
+ image.getAlphas(0, y, image.width, alpha, 0);
+ result.setAlphas(0, y, image.width, alpha, 0);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get column pixels.
+ * @return length of obtained pixels.
+ */
+ public static int getVerticalPixels(ImageData data, int x, int y, int height, int[] out) {
+ if (data == null) {
+ throw new IllegalArgumentException("data must not be null");
+ }
+ if (out == null) {
+ throw new IllegalArgumentException("out array must not be null");
+ }
+ if (height > out.length) {
+ throw new IllegalArgumentException("out array length must be > height");
+ }
+ if (data.height < (y + height)) {
+ throw new IllegalArgumentException("image height must be > (y + height)");
+ }
+ if (x < 0 || y < 0) {
+ throw new IllegalArgumentException("argument x, y must be >= 0");
+ }
+ if (x >= data.width) {
+ throw new IllegalArgumentException("argument x must be < data.width");
+ }
+ if (y >= data.height) {
+ throw new IllegalArgumentException("argument y must be < data.height");
+ }
+ if (height <= 0) {
+ throw new IllegalArgumentException("argument height must be > 0");
+ }
+
+ int idx = 0;
+ while (idx < height) {
+ data.getPixels(x, (y + idx), 1, out, idx);
+ idx++;
+ }
+ return idx;
+ }
+
+ /**
+ * Get row pixels.
+ */
+ public static void getHorizontalPixels(ImageData data, int x, int y, int width, int[] out) {
+ if (data == null) {
+ throw new IllegalArgumentException("data must not be null");
+ }
+ if (out == null) {
+ throw new IllegalArgumentException("out array must not be null");
+ }
+ if (width > out.length) {
+ throw new IllegalArgumentException("out array length must be > width");
+ }
+ if (data.width < (x + width)) {
+ throw new IllegalArgumentException("image height must be > (x + width)");
+ }
+ if (x < 0 || y < 0) {
+ throw new IllegalArgumentException("argument x, y must be >= 0");
+ }
+ if (x >= data.width) {
+ throw new IllegalArgumentException("argument x must be < data.width");
+ }
+ if (y >= data.height) {
+ throw new IllegalArgumentException("argument y must be < data.height");
+ }
+ if (width <= 0) {
+ throw new IllegalArgumentException("argument width must be > 0");
+ }
+
+ data.getPixels(x, y, width, out, 0);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java
new file mode 100644
index 000000000..f1022c3a2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java
@@ -0,0 +1,882 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics;
+
+import static com.android.SdkConstants.DOT_9PNG;
+import static com.android.SdkConstants.DOT_PNG;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The model of 9-patched image.
+ */
+public class NinePatchedImage {
+ private static final boolean DEBUG = false;
+
+ /**
+ * Get 9-patched filename as like image.9.png .
+ */
+ public static String getNinePatchedFileName(String fileName) {
+ if (fileName.endsWith(DOT_9PNG)) {
+ return fileName;
+ }
+ return fileName.substring(0, fileName.lastIndexOf(DOT_PNG)) + DOT_9PNG;
+ }
+
+ // For stretch regions and padding
+ public static final int BLACK_TICK = 0xFF000000;
+ // For Layout Bounds
+ public static final int RED_TICK = 0xFFFF0000;
+ // Blank
+ public static final int TRANSPARENT_TICK = 0x00000000;
+
+ private ImageData mBaseImageData;
+
+ private Image mBaseImage = null;
+
+ private boolean mHasNinePatchExtension = false;
+
+ private boolean mDirtyFlag = false;
+
+ private int[] mHorizontalPatchPixels = null;
+ private int[] mVerticalPatchPixels = null;
+
+ private int[] mHorizontalContentPixels = null;
+ private int[] mVerticalContentPixels = null;
+
+ // for Prevent unexpected stretch in StretchsView
+ private boolean mRedTickOnlyInHorizontalFlag = false;
+ private boolean mRedTickOnlyInVerticalFlag = false;
+
+ private final List<Tick> mHorizontalPatches = new ArrayList<Tick>();
+ private final List<Tick> mVerticalPatches = new ArrayList<Tick>();
+
+ private final List<Tick> mHorizontalContents = new ArrayList<Tick>();
+ private final List<Tick> mVerticalContents = new ArrayList<Tick>();
+
+
+ private static final int CHUNK_BIN_SIZE = 100;
+ private final List<Chunk> mChunkBin = new ArrayList<Chunk>(CHUNK_BIN_SIZE);
+
+ private int mHorizontalFixedPatchSum = 0;
+ private int mVerticalFixedPatchSum = 0;
+
+ private static final int PROJECTION_BIN_SIZE = 100;
+ private final List<Projection> mProjectionBin = new ArrayList<Projection>(PROJECTION_BIN_SIZE);
+
+ private Chunk[][] mPatchChunks = null;
+
+ public ImageData getImageData() {
+ return mBaseImageData;
+ }
+
+ public int getWidth() {
+ return mBaseImageData.width;
+ }
+
+ public int getHeight() {
+ return mBaseImageData.height;
+ }
+
+ public Image getImage() {
+ if (mBaseImage == null) {
+ mBaseImage = new Image(AdtPlugin.getDisplay(), mBaseImageData);
+ }
+ return mBaseImage;
+ }
+
+ public boolean hasNinePatchExtension() {
+ return mHasNinePatchExtension;
+ }
+
+ /**
+ * Get the image has/hasn't been edited flag.
+ * @return If has been edited, return true
+ */
+ public boolean isDirty() {
+ return mDirtyFlag;
+ }
+
+ /**
+ * Clear dirty(edited) flag.
+ */
+ public void clearDirtyFlag() {
+ mDirtyFlag = false;
+ }
+
+ public NinePatchedImage(String fileName) {
+ boolean hasNinePatchExtension = fileName.endsWith(DOT_9PNG);
+ ImageData data = new ImageData(fileName);
+
+ initNinePatchedImage(data, hasNinePatchExtension);
+ }
+
+ public NinePatchedImage(InputStream inputStream, String fileName) {
+ boolean hasNinePatchExtension = fileName.endsWith(DOT_9PNG);
+ ImageData data = new ImageData(inputStream);
+
+ initNinePatchedImage(data, hasNinePatchExtension);
+ }
+
+ private Chunk getChunk() {
+ if (mChunkBin.size() > 0) {
+ Chunk chunk = mChunkBin.remove(0);
+ chunk.init();
+ return chunk;
+ }
+ return new Chunk();
+ }
+
+ private static final void recycleChunks(Chunk[][] patchChunks, List<Chunk> bin) {
+ int yLen = patchChunks.length;
+ int xLen = patchChunks[0].length;
+
+ for (int y = 0; y < yLen; y++) {
+ for (int x = 0; x < xLen; x++) {
+ if (bin.size() < CHUNK_BIN_SIZE) {
+ bin.add(patchChunks[y][x]);
+ }
+ patchChunks[y][x] = null;
+ }
+ }
+ }
+
+ private Projection getProjection() {
+ if (mProjectionBin.size() > 0) {
+ Projection projection = mProjectionBin.remove(0);
+ return projection;
+ }
+ return new Projection();
+ }
+
+ private static final void recycleProjections(Projection[][] projections, List<Projection> bin) {
+ int yLen = projections.length;
+ int xLen = 0;
+ if (yLen > 0) {
+ xLen = projections[0].length;
+ }
+
+ for (int y = 0; y < yLen; y++) {
+ for (int x = 0; x < xLen; x++) {
+ if (bin.size() < CHUNK_BIN_SIZE) {
+ bin.add(projections[y][x]);
+ }
+ projections[y][x] = null;
+ }
+ }
+ }
+
+ private static final int[] initArray(int[] array) {
+ int len = array.length;
+ for (int i = 0; i < len; i++) {
+ array[i] = TRANSPARENT_TICK;
+ }
+ return array;
+ }
+
+ /**
+ * Get one pixel with alpha from the image.
+ * @return packed integer value as ARGB8888
+ */
+ private static final int getPixel(ImageData image, int x, int y) {
+ return (image.getAlpha(x, y) << 24) + image.getPixel(x, y);
+ }
+
+ private static final boolean isTransparentPixel(ImageData image, int x, int y) {
+ return image.getAlpha(x, y) == 0x0;
+ }
+
+ private static final boolean isValidTickColor(int pixel) {
+ return (pixel == BLACK_TICK || pixel == RED_TICK);
+ }
+
+ private void initNinePatchedImage(ImageData imageData, boolean hasNinePatchExtension) {
+ mBaseImageData = imageData;
+ mHasNinePatchExtension = hasNinePatchExtension;
+ }
+
+ private boolean ensurePixel(int x, int y, int[] pixels, int index) {
+ boolean isValid = true;
+ int pixel = getPixel(mBaseImageData, x, y);
+ if (!isTransparentPixel(mBaseImageData, x, y)) {
+ if (index == 0 || index == pixels.length - 1) {
+ isValid = false;
+ }
+ if (isValidTickColor(pixel)) {
+ pixels[index] = pixel;
+ } else {
+ isValid = false;
+ }
+ // clear pixel
+ mBaseImageData.setAlpha(x, y, 0x0);
+ }
+ return isValid;
+ }
+
+ private boolean ensureHorizontalPixel(int x, int y, int[] pixels) {
+ return ensurePixel(x, y, pixels, x);
+ }
+
+ private boolean ensureVerticalPixel(int x, int y, int[] pixels) {
+ return ensurePixel(x, y, pixels, y);
+ }
+
+ /**
+ * Ensure that image data is 9-patch.
+ */
+ public boolean ensure9Patch() {
+ boolean isValid = true;
+
+ int width = mBaseImageData.width;
+ int height = mBaseImageData.height;
+
+ createPatchArray();
+ createContentArray();
+
+ // horizontal
+ for (int x = 0; x < width; x++) {
+ // top row
+ if (!ensureHorizontalPixel(x, 0, mHorizontalPatchPixels)) {
+ isValid = false;
+ }
+ // bottom row
+ if (!ensureHorizontalPixel(x, height - 1, mHorizontalContentPixels)) {
+ isValid = false;
+ }
+ }
+ // vertical
+ for (int y = 0; y < height; y++) {
+ // left column
+ if (!ensureVerticalPixel(0, y, mVerticalPatchPixels)) {
+ isValid = false;
+ }
+ // right column
+ if (!ensureVerticalPixel(width -1, y, mVerticalContentPixels)) {
+ isValid = false;
+ }
+ }
+ findPatches();
+ findContentsArea();
+
+ return isValid;
+ }
+
+ private void createPatchArray() {
+ mHorizontalPatchPixels = initArray(new int[mBaseImageData.width]);
+ mVerticalPatchPixels = initArray(new int[mBaseImageData.height]);
+ }
+
+ private void createContentArray() {
+ mHorizontalContentPixels = initArray(new int[mBaseImageData.width]);
+ mVerticalContentPixels = initArray(new int[mBaseImageData.height]);
+ }
+
+ /**
+ * Convert to 9-patch image.
+ * <p>
+ * This method doesn't consider that target image is already 9-patched or
+ * not.
+ * </p>
+ */
+ public void convertToNinePatch() {
+ mBaseImageData = GraphicsUtilities.convertToNinePatch(mBaseImageData);
+ mHasNinePatchExtension = true;
+
+ createPatchArray();
+ createContentArray();
+
+ findPatches();
+ findContentsArea();
+ }
+
+ public boolean isValid(int x, int y) {
+ return (x == 0) ^ (y == 0)
+ ^ (x == mBaseImageData.width - 1) ^ (y == mBaseImageData.height - 1);
+ }
+
+ /**
+ * Set patch or content.
+ */
+ public void setPatch(int x, int y, int color) {
+ if (isValid(x, y)) {
+ if (x == 0) {
+ mVerticalPatchPixels[y] = color;
+ } else if (y == 0) {
+ mHorizontalPatchPixels[x] = color;
+ } else if (x == mBaseImageData.width - 1) {
+ mVerticalContentPixels[y] = color;
+ } else if (y == mBaseImageData.height - 1) {
+ mHorizontalContentPixels[x] = color;
+ }
+
+ // Mark as dirty
+ mDirtyFlag = true;
+ }
+ }
+
+ /**
+ * Erase the pixel.
+ */
+ public void erase(int x, int y) {
+ if (isValid(x, y)) {
+ int color = TRANSPARENT_TICK;
+ if (x == 0) {
+ mVerticalPatchPixels[y] = color;
+ } else if (y == 0) {
+ mHorizontalPatchPixels[x] = color;
+ } else if (x == mBaseImageData.width - 1) {
+ mVerticalContentPixels[y] = color;
+ } else if (y == mBaseImageData.height - 1) {
+ mHorizontalContentPixels[x] = color;
+ }
+
+ // Mark as dirty
+ mDirtyFlag = true;
+ }
+ }
+
+ public List<Tick> getHorizontalPatches() {
+ return mHorizontalPatches;
+ }
+
+ public List<Tick> getVerticalPatches() {
+ return mVerticalPatches;
+ }
+
+ /**
+ * Find patches from pixels array.
+ * @param pixels Target of seeking ticks.
+ * @param out Add the found ticks.
+ * @return If BlackTick is not found but only RedTick is found, returns true
+ */
+ private static boolean findPatches(int[] pixels, List<Tick> out) {
+ boolean redTickOnly = true;
+ Tick patch = null;
+ int len = 0;
+
+ // find patches
+ out.clear();
+ len = pixels.length - 1;
+ for (int i = 1; i < len; i++) {
+ int pixel = pixels[i];
+
+ if (redTickOnly && pixel != TRANSPARENT_TICK && pixel != RED_TICK) {
+ redTickOnly = false;
+ }
+
+ if (patch != null) {
+ if (patch.color != pixel) {
+ patch.end = i;
+ out.add(patch);
+ patch = null;
+ }
+ }
+ if (patch == null) {
+ patch = new Tick(pixel);
+ patch.start = i;
+ }
+ }
+
+ if (patch != null) {
+ patch.end = len;
+ out.add(patch);
+ }
+ return redTickOnly;
+ }
+
+ public void findPatches() {
+
+ // find horizontal patches
+ mRedTickOnlyInHorizontalFlag = findPatches(mHorizontalPatchPixels, mHorizontalPatches);
+
+ // find vertical patches
+ mRedTickOnlyInVerticalFlag = findPatches(mVerticalPatchPixels, mVerticalPatches);
+ }
+
+ public Rectangle getContentArea() {
+ Tick horizontal = getContentArea(mHorizontalContents);
+ Tick vertical = getContentArea(mVerticalContents);
+
+ Rectangle rect = new Rectangle(0, 0, 0, 0);
+ rect.x = 1;
+ rect.width = mBaseImageData.width - 1;
+ rect.y = 1;
+ rect.height = mBaseImageData.height - 1;
+
+ if (horizontal != null) {
+ rect.x = horizontal.start;
+ rect.width = horizontal.getLength();
+ }
+ if (vertical != null) {
+ rect.y = vertical.start;
+ rect.height = vertical.getLength();
+ }
+
+ return rect;
+ }
+
+ private Tick getContentArea(List<Tick> list) {
+ int size = list.size();
+ if (size == 0) {
+ return null;
+ }
+ if (size == 1) {
+ return list.get(0);
+ }
+
+ Tick start = null;
+ Tick end = null;
+
+ for (int i = 0; i < size; i++) {
+ Tick t = list.get(i);
+ if (t.color == BLACK_TICK) {
+ if (start == null) {
+ start = t;
+ end = t;
+ } else {
+ end = t;
+ }
+ }
+ }
+
+ // red tick only
+ if (start == null) {
+ return null;
+ }
+
+ Tick result = new Tick(start.color);
+ result.start = start.start;
+ result.end = end.end;
+
+ return result;
+ }
+
+ /**
+ * This is for unit test use only.
+ * @see com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImageTest
+ */
+ public List<Tick> getHorizontalContents() {
+ return mHorizontalContents;
+ }
+
+ /**
+ * This is for unit test use only.
+ * @see com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImageTest
+ */
+ public List<Tick> getVerticalContents() {
+ return mVerticalContents;
+ }
+
+ private static void findContentArea(int[] pixels, List<Tick> out) {
+ Tick contents = null;
+ int len = 0;
+
+ // find horizontal contents area
+ out.clear();
+ len = pixels.length - 1;
+ for (int x = 1; x < len; x++) {
+ if (contents != null) {
+ if (contents.color != pixels[x]) {
+ contents.end = x;
+ out.add(contents);
+ contents = null;
+ }
+ }
+ if (contents == null) {
+ contents = new Tick(pixels[x]);
+ contents.start = x;
+ }
+ }
+
+ if (contents != null) {
+ contents.end = len;
+ out.add(contents);
+ }
+ }
+
+ public void findContentsArea() {
+
+ // find horizontal contents area
+ findContentArea(mHorizontalContentPixels, mHorizontalContents);
+
+ // find vertical contents area
+ findContentArea(mVerticalContentPixels, mVerticalContents);
+ }
+
+ /**
+ * Get raw image data.
+ * <p>
+ * The raw image data is applicable for save.
+ * </p>
+ */
+ public ImageData getRawImageData() {
+ ImageData image = GraphicsUtilities.copy(mBaseImageData);
+
+ final int width = image.width;
+ final int height = image.height;
+ int len = 0;
+
+ len = mHorizontalPatchPixels.length;
+ for (int x = 0; x < len; x++) {
+ int pixel = mHorizontalPatchPixels[x];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(x, 0, 0xFF);
+ image.setPixel(x, 0, pixel);
+ }
+ }
+
+ len = mVerticalPatchPixels.length;
+ for (int y = 0; y < len; y++) {
+ int pixel = mVerticalPatchPixels[y];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(0, y, 0xFF);
+ image.setPixel(0, y, pixel);
+ }
+ }
+
+ len = mHorizontalContentPixels.length;
+ for (int x = 0; x < len; x++) {
+ int pixel = mHorizontalContentPixels[x];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(x, height - 1, 0xFF);
+ image.setPixel(x, height - 1, pixel);
+ }
+ }
+
+ len = mVerticalContentPixels.length;
+ for (int y = 0; y < len; y++) {
+ int pixel = mVerticalContentPixels[y];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(width - 1, y, 0xFF);
+ image.setPixel(width - 1, y, pixel);
+ }
+ }
+
+ return image;
+ }
+
+ public Chunk[][] getChunks(Chunk[][] chunks) {
+ int lenY = mVerticalPatches.size();
+ int lenX = mHorizontalPatches.size();
+
+ if (lenY == 0 || lenX == 0) {
+ return null;
+ }
+
+ if (chunks == null) {
+ chunks = new Chunk[lenY][lenX];
+ } else {
+ int y = chunks.length;
+ int x = chunks[0].length;
+ if (lenY != y || lenX != x) {
+ recycleChunks(chunks, mChunkBin);
+ chunks = new Chunk[lenY][lenX];
+ }
+ }
+
+ // for calculate weights
+ float horizontalPatchSum = 0;
+ float verticalPatchSum = 0;
+
+ mVerticalFixedPatchSum = 0;
+ mHorizontalFixedPatchSum = 0;
+
+ for (int y = 0; y < lenY; y++) {
+ Tick yTick = mVerticalPatches.get(y);
+
+ for (int x = 0; x < lenX; x++) {
+ Tick xTick = mHorizontalPatches.get(x);
+ Chunk t = getChunk();
+ chunks[y][x] = t;
+
+ t.rect.x = xTick.start;
+ t.rect.width = xTick.getLength();
+ t.rect.y = yTick.start;
+ t.rect.height = yTick.getLength();
+
+ if (mRedTickOnlyInHorizontalFlag
+ || xTick.color == BLACK_TICK || lenX == 1) {
+ t.type += Chunk.TYPE_HORIZONTAL;
+ if (y == 0) {
+ horizontalPatchSum += t.rect.width;
+ }
+ }
+ if (mRedTickOnlyInVerticalFlag
+ || yTick.color == BLACK_TICK || lenY == 1) {
+ t.type += Chunk.TYPE_VERTICAL;
+ if (x == 0) {
+ verticalPatchSum += t.rect.height;
+ }
+ }
+
+ if ((t.type & Chunk.TYPE_HORIZONTAL) == 0 && lenX > 1 && y == 0) {
+ mHorizontalFixedPatchSum += t.rect.width;
+ }
+ if ((t.type & Chunk.TYPE_VERTICAL) == 0 && lenY > 1 && x == 0) {
+ mVerticalFixedPatchSum += t.rect.height;
+ }
+
+ }
+ }
+
+ // calc weights
+ for (int y = 0; y < lenY; y++) {
+ for (int x = 0; x < lenX; x++) {
+ Chunk chunk = chunks[y][x];
+ if ((chunk.type & Chunk.TYPE_HORIZONTAL) != 0) {
+ chunk.horizontalWeight = chunk.rect.width / horizontalPatchSum;
+ }
+ if ((chunk.type & Chunk.TYPE_VERTICAL) != 0) {
+ chunk.verticalWeight = chunk.rect.height / verticalPatchSum;
+
+ }
+ }
+ }
+
+ return chunks;
+ }
+
+ public Chunk[][] getCorruptedChunks(Chunk[][] chunks) {
+ chunks = getChunks(chunks);
+
+ if (chunks != null) {
+ int yLen = chunks.length;
+ int xLen = chunks[0].length;
+
+ for (int yPos = 0; yPos < yLen; yPos++) {
+ for (int xPos = 0; xPos < xLen; xPos++) {
+ Chunk c = chunks[yPos][xPos];
+ Rectangle r = c.rect;
+ if ((c.type & Chunk.TYPE_HORIZONTAL) != 0
+ && isHorizontalCorrupted(mBaseImageData, r)) {
+ c.type |= Chunk.TYPE_CORRUPT;
+ }
+ if ((c.type & Chunk.TYPE_VERTICAL) != 0
+ && isVerticalCorrupted(mBaseImageData, r)) {
+ c.type |= Chunk.TYPE_CORRUPT;
+ }
+ }
+ }
+ }
+ return chunks;
+ }
+
+ private static boolean isVerticalCorrupted(ImageData data, Rectangle r) {
+ int[] column = new int[r.width];
+ int[] sample = new int[r.width];
+
+ GraphicsUtilities.getHorizontalPixels(data, r.x, r.y, r.width, column);
+
+ int lenY = r.y + r.height;
+ for (int y = r.y; y < lenY; y++) {
+ GraphicsUtilities.getHorizontalPixels(data, r.x, y, r.width, sample);
+ if (!Arrays.equals(column, sample)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isHorizontalCorrupted(ImageData data, Rectangle r) {
+ int[] column = new int[r.height];
+ int[] sample = new int[r.height];
+ GraphicsUtilities.getVerticalPixels(data, r.x, r.y, r.height, column);
+
+ int lenX = r.x + r.width;
+ for (int x = r.x; x < lenX; x++) {
+ GraphicsUtilities.getVerticalPixels(data, x, r.y, r.height, sample);
+ if (!Arrays.equals(column, sample)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Projection[][] getProjections(int width, int height, Projection[][] projections) {
+ mPatchChunks = getChunks(mPatchChunks);
+ if (mPatchChunks == null) {
+ return null;
+ }
+
+ if (DEBUG) {
+ System.out.println(String.format("width:%d, height:%d", width, height));
+ }
+
+ int lenY = mPatchChunks.length;
+ int lenX = mPatchChunks[0].length;
+
+ if (projections == null) {
+ projections = new Projection[lenY][lenX];
+ } else {
+ int y = projections.length;
+ int x = projections[0].length;
+ if (lenY != y || lenX != x) {
+ recycleProjections(projections, mProjectionBin);
+ projections = new Projection[lenY][lenX];
+ }
+ }
+
+ float xZoom = ((float) width / mBaseImageData.width);
+ float yZoom = ((float) height / mBaseImageData.height);
+
+ if (DEBUG) {
+ System.out.println(String.format("xZoom:%f, yZoom:%f", xZoom, yZoom));
+ }
+
+ int destX = 0;
+ int destY = 0;
+ int streatchableWidth = width - mHorizontalFixedPatchSum;
+ streatchableWidth = streatchableWidth > 0 ? streatchableWidth : 1;
+
+ int streatchableHeight = height - mVerticalFixedPatchSum;
+ streatchableHeight = streatchableHeight > 0 ? streatchableHeight : 1;
+
+ if (DEBUG) {
+ System.out.println(String.format("streatchable %d %d", streatchableWidth,
+ streatchableHeight));
+ }
+
+ for (int yPos = 0; yPos < lenY; yPos++) {
+ destX = 0;
+ Projection p = null;
+ for (int xPos = 0; xPos < lenX; xPos++) {
+ Chunk chunk = mPatchChunks[yPos][xPos];
+
+ if (DEBUG) {
+ System.out.println(String.format("Tile[%d, %d] = %s",
+ yPos, xPos, chunk.toString()));
+ }
+
+ p = getProjection();
+ projections[yPos][xPos] = p;
+
+ p.chunk = chunk;
+ p.src = chunk.rect;
+ p.dest.x = destX;
+ p.dest.y = destY;
+
+ // fixed size
+ p.dest.width = chunk.rect.width;
+ p.dest.height = chunk.rect.height;
+
+ // horizontal stretch
+ if ((chunk.type & Chunk.TYPE_HORIZONTAL) != 0) {
+ p.dest.width = Math.round(streatchableWidth * chunk.horizontalWeight);
+ }
+ // vertical stretch
+ if ((chunk.type & Chunk.TYPE_VERTICAL) != 0) {
+ p.dest.height = Math.round(streatchableHeight * chunk.verticalWeight);
+ }
+
+ destX += p.dest.width;
+ }
+ destY += p.dest.height;
+ }
+ return projections;
+ }
+
+ /**
+ * Projection class for make relation between chunked image and resized image.
+ */
+ public static class Projection {
+ public Chunk chunk = null;
+ public Rectangle src = null;
+ public final Rectangle dest = new Rectangle(0, 0, 0, 0);
+
+ @Override
+ public String toString() {
+ return String.format("src[%d, %d, %d, %d] => dest[%d, %d, %d, %d]",
+ src.x, src.y, src.width, src.height,
+ dest.x, dest.y, dest.width, dest.height);
+ }
+ }
+
+ public static class Chunk {
+ public static final int TYPE_FIXED = 0x0;
+ public static final int TYPE_HORIZONTAL = 0x1;
+ public static final int TYPE_VERTICAL = 0x2;
+ public static final int TYPE_CORRUPT = 0x80000000;
+
+ public int type = TYPE_FIXED;
+
+ public Rectangle rect = new Rectangle(0, 0, 0, 0);
+
+ public float horizontalWeight = 0.0f;
+ public float verticalWeight = 0.0f;
+
+ void init() {
+ type = Chunk.TYPE_FIXED;
+ horizontalWeight = 0.0f;
+ verticalWeight = 0.0f;
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = 0;
+ rect.height = 0;
+ }
+
+ private String typeToString() {
+ switch (type) {
+ case TYPE_FIXED:
+ return "FIXED";
+ case TYPE_HORIZONTAL:
+ return "HORIZONTAL";
+ case TYPE_VERTICAL:
+ return "VERTICAL";
+ case TYPE_HORIZONTAL + TYPE_VERTICAL:
+ return "BOTH";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %f/%f %s", typeToString(), horizontalWeight, verticalWeight,
+ rect.toString());
+ }
+ }
+
+ public static class Tick {
+ public int start;
+ public int end;
+ public int color;
+
+ /**
+ * Get the tick length.
+ */
+ public int getLength() {
+ return end - start;
+ }
+
+ public Tick(int tickColor) {
+ color = tickColor;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%d tick: %d to %d", color, start, end);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java
new file mode 100644
index 000000000..7c4523024
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.ui;
+
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.FileTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Image editor pane.
+ */
+public class ImageEditorPanel extends Composite implements ImageViewer.UpdateListener,
+ StatusPanel.StatusChangedListener {
+
+ private static final int WEIGHT_VIEWER = 3;
+ private static final int WEIGHT_PREVIEW = 1;
+
+ private final ImageViewer mImageViewer;
+ private final StretchesViewer mStretchesViewer;
+
+ public ImageViewer getImageViewer() {
+ return mImageViewer;
+ }
+
+ public ImageEditorPanel(Composite parent, int style) {
+ super(parent, style);
+
+ setLayout(new FillLayout());
+ SashForm sashForm = new SashForm(this, SWT.HORIZONTAL);
+
+ mImageViewer = new ImageViewer(sashForm, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ mImageViewer.addUpdateListener(this);
+
+ mStretchesViewer = new StretchesViewer(sashForm, SWT.BORDER);
+
+ sashForm.setWeights(new int[] {
+ WEIGHT_VIEWER, WEIGHT_PREVIEW
+ });
+ }
+
+ @Override
+ public void zoomChanged(int zoom) {
+ mImageViewer.setZoom(zoom);
+ }
+
+ @Override
+ public void scaleChanged(int scale) {
+ mStretchesViewer.setScale(scale);
+ }
+
+ @Override
+ public void lockVisibilityChanged(boolean visible) {
+ mImageViewer.setShowLock(visible);
+ }
+
+ @Override
+ public void patchesVisibilityChanged(boolean visible) {
+ mImageViewer.setShowPatchesArea(visible);
+ }
+
+ @Override
+ public void badPatchesVisibilityChanged(boolean visible) {
+ mImageViewer.setShowBadPatchesArea(visible);
+ }
+
+ @Override
+ public void contentAreaVisibilityChanged(boolean visible) {
+ mStretchesViewer.setShowContentArea(visible);
+ }
+
+ @Override
+ public void update(NinePatchedImage image) {
+ mStretchesViewer.updatePreview(image);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java
new file mode 100644
index 000000000..2414a39d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage.Chunk;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage.Tick;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.ScrollBar;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+
+/**
+ * View and edit Draw 9-patch image.
+ */
+public class ImageViewer extends Canvas implements PaintListener, KeyListener, MouseListener,
+ MouseMoveListener {
+ private static final boolean DEBUG = false;
+
+ public static final String HELP_MESSAGE_KEY_TIPS = "Press Shift to erase pixels."
+ + " Press Control to draw layout bounds.";
+
+ public static final String HELP_MESSAGE_KEY_TIPS2 = "Release Shift to draw pixels.";
+
+ private static final Color BLACK_COLOR = AdtPlugin.getDisplay().getSystemColor(SWT.COLOR_BLACK);
+ private static final Color RED_COLOR = AdtPlugin.getDisplay().getSystemColor(SWT.COLOR_RED);
+
+ private static final Color BACK_COLOR
+ = new Color(AdtPlugin.getDisplay(), new RGB(0x00, 0xFF, 0x00));
+ private static final Color LOCK_COLOR
+ = new Color(AdtPlugin.getDisplay(), new RGB(0xFF, 0x00, 0x00));
+ private static final Color PATCH_COLOR
+ = new Color(AdtPlugin.getDisplay(), new RGB(0xFF, 0xFF, 0x00));
+ private static final Color PATCH_ONEWAY_COLOR
+ = new Color(AdtPlugin.getDisplay(), new RGB(0x00, 0x00, 0xFF));
+ private static final Color CORRUPTED_COLOR
+ = new Color(AdtPlugin.getDisplay(), new RGB(0xFF, 0x00, 0x00));
+
+ private static final int NONE_ALPHA = 0xFF;
+ private static final int LOCK_ALPHA = 50;
+ private static final int PATCH_ALPHA = 50;
+ private static final int GUIDE_ALPHA = 60;
+
+ private static final int MODE_NONE = 0x00;
+ private static final int MODE_BLACK_TICK = 0x01;
+ private static final int MODE_RED_TICK = 0x02;
+ private static final int MODE_ERASE = 0xFF;
+
+ private int mDrawMode = MODE_NONE;
+
+ private static final int MARGIN = 5;
+ private static final String CHECKER_PNG_PATH = "/icons/checker.png";
+
+ private Image mBackgroundLayer = null;
+
+ private NinePatchedImage mNinePatchedImage = null;
+
+ private Chunk[][] mChunks = null;
+ private Chunk[][] mBadChunks = null;
+
+ private boolean mIsLockShown = true;
+ private boolean mIsPatchesShown = false;
+ private boolean mIsBadPatchesShown = false;
+
+ private ScrollBar mHorizontalBar;
+ private ScrollBar mVerticalBar;
+
+ private int mZoom = 500;
+
+ private int mHorizontalScroll = 0;
+ private int mVerticalScroll = 0;
+
+ private final Rectangle mPadding = new Rectangle(0, 0, 0, 0);
+
+ // one pixel size that considered zoom
+ private int mZoomedPixelSize = 1;
+
+ private Image mBufferImage = null;
+
+ private boolean isCtrlPressed = false;
+ private boolean isShiftPressed = false;
+
+ private final List<UpdateListener> mUpdateListenerList
+ = new ArrayList<UpdateListener>();
+
+ private final Point mCursorPoint = new Point(0, 0);
+
+ private static final int DEFAULT_UPDATE_QUEUE_SIZE = 10;
+
+ private final ArrayBlockingQueue<NinePatchedImage> mUpdateQueue
+ = new ArrayBlockingQueue<NinePatchedImage>(DEFAULT_UPDATE_QUEUE_SIZE);
+
+ private final Runnable mUpdateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (isDisposed()) {
+ return;
+ }
+
+ redraw();
+
+ fireUpdateEvent();
+ }
+ };
+
+ private final Thread mUpdateThread = new Thread() {
+ @Override
+ public void run() {
+ while (!isDisposed()) {
+ try {
+ mUpdateQueue.take();
+ mNinePatchedImage.findPatches();
+ mNinePatchedImage.findContentsArea();
+
+ mChunks = mNinePatchedImage.getChunks(mChunks);
+ mBadChunks = mNinePatchedImage.getCorruptedChunks(mBadChunks);
+
+ AdtPlugin.getDisplay().asyncExec(mUpdateRunnable);
+
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ };
+
+ private StatusChangedListener mStatusChangedListener = null;
+
+ public void addUpdateListener(UpdateListener l) {
+ mUpdateListenerList.add(l);
+ }
+
+ public void removeUpdateListener(UpdateListener l) {
+ mUpdateListenerList.remove(l);
+ }
+
+ private void fireUpdateEvent() {
+ int len = mUpdateListenerList.size();
+ for(int i=0; i < len; i++) {
+ mUpdateListenerList.get(i).update(mNinePatchedImage);
+ }
+ }
+
+ public void setStatusChangedListener(StatusChangedListener l) {
+ mStatusChangedListener = l;
+ if (mStatusChangedListener != null) {
+ mStatusChangedListener.helpTextChanged(HELP_MESSAGE_KEY_TIPS);
+ }
+ }
+
+ void setShowLock(boolean show) {
+ mIsLockShown = show;
+ redraw();
+ }
+
+ void setShowPatchesArea(boolean show) {
+ mIsPatchesShown = show;
+ redraw();
+ }
+
+ void setShowBadPatchesArea(boolean show) {
+ mIsBadPatchesShown = show;
+ redraw();
+ }
+
+ void setZoom(int zoom) {
+ mZoom = zoom;
+ mZoomedPixelSize = getZoomedPixelSize(1);
+ redraw();
+ }
+
+ public ImageViewer(Composite parent, int style) {
+ super(parent, style);
+
+ mUpdateThread.start();
+
+ mBackgroundLayer = AdtPlugin.getImageDescriptor(CHECKER_PNG_PATH).createImage();
+
+ addMouseListener(this);
+ addMouseMoveListener(this);
+ addPaintListener(this);
+
+ mHorizontalBar = getHorizontalBar();
+ mHorizontalBar.setThumb(1);
+ mHorizontalBar.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ ScrollBar bar = (ScrollBar) event.widget;
+ if (mHorizontalBar.isVisible()
+ && mHorizontalScroll != bar.getSelection()) {
+ mHorizontalScroll = bar.getSelection();
+ redraw();
+ }
+ }
+ });
+
+ mVerticalBar = getVerticalBar();
+ mVerticalBar.setThumb(1);
+ mVerticalBar.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ ScrollBar bar = (ScrollBar) event.widget;
+ if (mVerticalBar.isVisible()
+ && mVerticalScroll != bar.getSelection()) {
+ mVerticalScroll = bar.getSelection();
+ redraw();
+ }
+ }
+ });
+ }
+
+ /**
+ * Load the image file.
+ *
+ * @param fileName must be absolute path
+ */
+ public NinePatchedImage loadFile(String fileName) {
+ mNinePatchedImage = new NinePatchedImage(fileName);
+
+ return mNinePatchedImage;
+ }
+
+ /**
+ * Start displaying the image.
+ */
+ public void startDisplay() {
+ mZoomedPixelSize = getZoomedPixelSize(1);
+
+ scheduleUpdate();
+ }
+
+ private void draw(int x, int y, int drawMode) {
+ if (drawMode == MODE_ERASE) {
+ erase(x, y);
+ } else {
+ int color = (drawMode == MODE_RED_TICK) ? NinePatchedImage.RED_TICK
+ : NinePatchedImage.BLACK_TICK;
+ mNinePatchedImage.setPatch(x, y, color);
+ redraw();
+
+ scheduleUpdate();
+ }
+ }
+
+ private void erase(int x, int y) {
+ mNinePatchedImage.erase(x, y);
+ redraw();
+
+ scheduleUpdate();
+ }
+
+ private void scheduleUpdate() {
+ try {
+ mUpdateQueue.put(mNinePatchedImage);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ @Override
+ public void mouseDown(MouseEvent event) {
+ if (event.button == 1 && !isShiftPressed) {
+ mDrawMode = !isCtrlPressed ? MODE_BLACK_TICK : MODE_RED_TICK;
+ draw(mCursorPoint.x, mCursorPoint.y, mDrawMode);
+ } else if (event.button == 3 || isShiftPressed) {
+ mDrawMode = MODE_ERASE;
+ erase(mCursorPoint.x, mCursorPoint.y);
+ }
+ }
+
+ @Override
+ public void mouseUp(MouseEvent event) {
+ mDrawMode = MODE_NONE;
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent event) {
+ }
+
+ private int getLogicalPositionX(int x) {
+ return Math.round((x - mPadding.x + mHorizontalScroll) / ((float) mZoom / 100));
+ }
+
+ private int getLogicalPositionY(int y) {
+ return Math.round((y - mPadding.y + mVerticalScroll) / ((float) mZoom / 100));
+ }
+
+ @Override
+ public void mouseMove(MouseEvent event) {
+ int posX = getLogicalPositionX(event.x);
+ int posY = getLogicalPositionY(event.y);
+
+ int width = mNinePatchedImage.getWidth();
+ int height = mNinePatchedImage.getHeight();
+
+ if (posX < 0) {
+ posX = 0;
+ }
+ if (posX >= width) {
+ posX = width - 1;
+ }
+ if (posY < 0) {
+ posY = 0;
+ }
+ if (posY >= height) {
+ posY = height - 1;
+ }
+
+ if (mDrawMode != MODE_NONE) {
+ int drawMode = mDrawMode;
+ if (isShiftPressed) {
+ drawMode = MODE_ERASE;
+ } else if (mDrawMode != MODE_ERASE) {
+ drawMode = !isCtrlPressed ? MODE_BLACK_TICK : MODE_RED_TICK;
+ }
+
+ /*
+ * Consider the previous cursor position because mouseMove events are
+ * scatter.
+ */
+ int x = mCursorPoint.x;
+ int y = mCursorPoint.y;
+ for (; y != posY; y += (y > posY) ? -1 : 1) {
+ draw(x, y, drawMode);
+ }
+
+ x = mCursorPoint.x;
+ y = mCursorPoint.y;
+ for (; x != posX; x += (x > posX) ? -1 : 1) {
+ draw(x, y, drawMode);
+ }
+ }
+ mCursorPoint.x = posX;
+ mCursorPoint.y = posY;
+
+ redraw();
+
+ if (mStatusChangedListener != null) {
+ // Update position on status panel if position is in logical size.
+ if (posX >= 0 && posY >= 0
+ && posX <= mNinePatchedImage.getWidth()
+ && posY <= mNinePatchedImage.getHeight()) {
+ mStatusChangedListener.cursorPositionChanged(posX + 1, posY + 1);
+ }
+ }
+ }
+
+ private synchronized void calcPaddings(int width, int height) {
+ Point canvasSize = getSize();
+
+ mPadding.width = getZoomedPixelSize(width);
+ mPadding.height = getZoomedPixelSize(height);
+
+ int margin = getZoomedPixelSize(MARGIN);
+
+ if (mPadding.width < canvasSize.x) {
+ mPadding.x = (canvasSize.x - mPadding.width) / 2;
+ } else {
+ mPadding.x = margin;
+ }
+
+ if (mPadding.height < canvasSize.y) {
+ mPadding.y = (canvasSize.y - mPadding.height) / 2;
+ } else {
+ mPadding.y = margin;
+ }
+ }
+
+ private void calcScrollBarSettings() {
+ Point size = getSize();
+ int screenWidth = size.x;
+ int screenHeight = size.y;
+
+ int imageWidth = getZoomedPixelSize(mNinePatchedImage.getWidth() + 1);
+ int imageHeight = getZoomedPixelSize(mNinePatchedImage.getHeight() + 1);
+
+ // consider the scroll bar sizes
+ int verticalBarSize = mVerticalBar.getSize().x;
+ int horizontalBarSize = mHorizontalBar.getSize().y;
+
+ int horizontalScroll = imageWidth - (screenWidth - verticalBarSize);
+ int verticalScroll = imageHeight - (screenHeight - horizontalBarSize);
+
+ int margin = getZoomedPixelSize(MARGIN) * 2;
+
+ if (horizontalScroll > 0) {
+ mHorizontalBar.setVisible(true);
+
+ // horizontal maximum
+ int max = horizontalScroll + verticalBarSize + margin;
+ mHorizontalBar.setMaximum(max);
+
+ // set corrected scroll size
+ int value = mHorizontalBar.getSelection();
+ value = max < value ? max : value;
+
+ mHorizontalBar.setSelection(value);
+ mHorizontalScroll = value;
+
+ } else {
+ mHorizontalBar.setSelection(0);
+ mHorizontalBar.setMaximum(0);
+ mHorizontalBar.setVisible(false);
+ }
+
+ if (verticalScroll > 0) {
+ mVerticalBar.setVisible(true);
+
+ // vertical maximum
+ int max = verticalScroll + horizontalBarSize + margin;
+ mVerticalBar.setMaximum(max);
+
+ // set corrected scroll size
+ int value = mVerticalBar.getSelection();
+ value = max < value ? max : value;
+
+ mVerticalBar.setSelection(value);
+ mVerticalScroll = value;
+
+ } else {
+ mVerticalBar.setSelection(0);
+ mVerticalBar.setMaximum(0);
+ mVerticalBar.setVisible(false);
+ }
+ }
+
+ private int getZoomedPixelSize(int val) {
+ return Math.round(val * (float) mZoom / 100);
+ }
+
+ @Override
+ public void paintControl(PaintEvent pe) {
+ if (mNinePatchedImage == null) {
+ return;
+ }
+
+ // Use buffer
+ GC bufferGc = null;
+ if (mBufferImage == null) {
+ mBufferImage = new Image(AdtPlugin.getDisplay(), pe.width, pe.height);
+ } else {
+ int width = mBufferImage.getBounds().width;
+ int height = mBufferImage.getBounds().height;
+ if (width != pe.width || height != pe.height) {
+ mBufferImage = new Image(AdtPlugin.getDisplay(), pe.width, pe.height);
+ }
+ }
+
+ // Draw previous image once for prevent flicking
+ pe.gc.drawImage(mBufferImage, 0, 0);
+
+ bufferGc = new GC(mBufferImage);
+ bufferGc.setAdvanced(true);
+
+ // Make interpolation disable
+ bufferGc.setInterpolation(SWT.NONE);
+
+ // clear buffer
+ bufferGc.fillRectangle(0, 0, pe.width, pe.height);
+
+ calcScrollBarSettings();
+
+ // padding from current zoom
+ int width = mNinePatchedImage.getWidth();
+ int height = mNinePatchedImage.getHeight();
+ calcPaddings(width, height);
+
+ int baseX = mPadding.x - mHorizontalScroll;
+ int baseY = mPadding.y - mVerticalScroll;
+
+ // draw checker image
+ bufferGc.drawImage(mBackgroundLayer,
+ 0, 0, mBackgroundLayer.getImageData().width,
+ mBackgroundLayer.getImageData().height,
+ baseX, baseY, mPadding.width, mPadding.height);
+
+ if (DEBUG) {
+ System.out.println(String.format("%d,%d %d,%d %d,%d",
+ width, height, baseX, baseY, mPadding.width, mPadding.height));
+ }
+
+ // draw image
+ /* TODO: Do not draw invisible area, for better performance. */
+ bufferGc.drawImage(mNinePatchedImage.getImage(), 0, 0, width, height, baseX, baseY,
+ mPadding.width, mPadding.height);
+
+ bufferGc.setBackground(BLACK_COLOR);
+
+ // draw patch ticks
+ drawHorizontalPatches(bufferGc, baseX, baseY);
+ drawVerticalPatches(bufferGc, baseX, baseY);
+
+ // draw content ticks
+ drawHorizontalContentArea(bufferGc, baseX, baseY);
+ drawVerticalContentArea(bufferGc, baseX, baseY);
+
+ if (mNinePatchedImage.isValid(mCursorPoint.x, mCursorPoint.y)) {
+ bufferGc.setForeground(BLACK_COLOR);
+ } else if (mIsLockShown) {
+ drawLockArea(bufferGc, baseX, baseY);
+ }
+
+ // Patches
+ if (mIsPatchesShown) {
+ drawPatchAreas(bufferGc, baseX, baseY);
+ }
+
+ // Bad patches
+ if (mIsBadPatchesShown) {
+ drawBadPatchAreas(bufferGc, baseX, baseY);
+ }
+
+ if (mNinePatchedImage.isValid(mCursorPoint.x, mCursorPoint.y)) {
+ bufferGc.setForeground(BLACK_COLOR);
+ } else {
+ bufferGc.setForeground(RED_COLOR);
+ }
+
+ drawGuideLine(bufferGc, baseX, baseY);
+
+ bufferGc.dispose();
+
+ pe.gc.drawImage(mBufferImage, 0, 0);
+ }
+
+ private static final Color getColor(int color) {
+ switch (color) {
+ case NinePatchedImage.RED_TICK:
+ return RED_COLOR;
+ default:
+ return BLACK_COLOR;
+ }
+ }
+
+ private void drawVerticalPatches(GC gc, int baseX, int baseY) {
+ List<Tick> verticalPatches = mNinePatchedImage.getVerticalPatches();
+ for (Tick t : verticalPatches) {
+ if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
+ gc.setBackground(getColor(t.color));
+ gc.fillRectangle(
+ baseX,
+ baseY + getZoomedPixelSize(t.start),
+ mZoomedPixelSize,
+ getZoomedPixelSize(t.getLength()));
+ }
+ }
+ }
+
+ private void drawHorizontalPatches(GC gc, int baseX, int baseY) {
+ List<Tick> horizontalPatches = mNinePatchedImage.getHorizontalPatches();
+ for (Tick t : horizontalPatches) {
+ if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
+ gc.setBackground(getColor(t.color));
+ gc.fillRectangle(
+ baseX + getZoomedPixelSize(t.start),
+ baseY,
+ getZoomedPixelSize(t.getLength()),
+ mZoomedPixelSize);
+ }
+ }
+ }
+
+ private void drawHorizontalContentArea(GC gc, int baseX, int baseY) {
+ List<Tick> horizontalContentArea = mNinePatchedImage.getHorizontalContents();
+ for (Tick t : horizontalContentArea) {
+ if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
+ gc.setBackground(getColor(t.color));
+ gc.fillRectangle(
+ baseX + getZoomedPixelSize(t.start),
+ baseY + getZoomedPixelSize(mNinePatchedImage.getHeight() - 1),
+ getZoomedPixelSize(t.getLength()),
+ mZoomedPixelSize);
+ }
+ }
+
+ }
+
+ private void drawVerticalContentArea(GC gc, int baseX, int baseY) {
+ List<Tick> verticalContentArea = mNinePatchedImage.getVerticalContents();
+ for (Tick t : verticalContentArea) {
+ if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
+ gc.setBackground(getColor(t.color));
+ gc.fillRectangle(
+ baseX + getZoomedPixelSize(mNinePatchedImage.getWidth() - 1),
+ baseY + getZoomedPixelSize(t.start),
+ mZoomedPixelSize,
+ getZoomedPixelSize(t.getLength()));
+ }
+ }
+ }
+
+ private void drawLockArea(GC gc, int baseX, int baseY) {
+ gc.setAlpha(LOCK_ALPHA);
+ gc.setForeground(LOCK_COLOR);
+ gc.setBackground(LOCK_COLOR);
+
+ gc.fillRectangle(
+ baseX + mZoomedPixelSize,
+ baseY + mZoomedPixelSize,
+ getZoomedPixelSize(mNinePatchedImage.getWidth() - 2),
+ getZoomedPixelSize(mNinePatchedImage.getHeight() - 2));
+ gc.setAlpha(NONE_ALPHA);
+
+ }
+
+ private void drawPatchAreas(GC gc, int baseX, int baseY) {
+ if (mChunks != null) {
+ int yLen = mChunks.length;
+ int xLen = mChunks[0].length;
+
+ gc.setAlpha(PATCH_ALPHA);
+
+ for (int yPos = 0; yPos < yLen; yPos++) {
+ for (int xPos = 0; xPos < xLen; xPos++) {
+ Chunk c = mChunks[yPos][xPos];
+
+ if (c.type == Chunk.TYPE_FIXED) {
+ gc.setBackground(BACK_COLOR);
+ } else if (c.type == Chunk.TYPE_HORIZONTAL) {
+ gc.setBackground(PATCH_ONEWAY_COLOR);
+ } else if (c.type == Chunk.TYPE_VERTICAL) {
+ gc.setBackground(PATCH_ONEWAY_COLOR);
+ } else if (c.type == Chunk.TYPE_HORIZONTAL + Chunk.TYPE_VERTICAL) {
+ gc.setBackground(PATCH_COLOR);
+ }
+ Rectangle r = c.rect;
+ gc.fillRectangle(
+ baseX + getZoomedPixelSize(r.x),
+ baseY + getZoomedPixelSize(r.y),
+ getZoomedPixelSize(r.width),
+ getZoomedPixelSize(r.height));
+ }
+ }
+ }
+ gc.setAlpha(NONE_ALPHA);
+ }
+
+ private void drawBadPatchAreas(GC gc, int baseX, int baseY) {
+ if (mBadChunks != null) {
+ int yLen = mBadChunks.length;
+ int xLen = mBadChunks[0].length;
+
+ gc.setAlpha(NONE_ALPHA);
+ gc.setForeground(CORRUPTED_COLOR);
+ gc.setBackground(CORRUPTED_COLOR);
+
+ for (int yPos = 0; yPos < yLen; yPos++) {
+ for (int xPos = 0; xPos < xLen; xPos++) {
+ Chunk c = mBadChunks[yPos][xPos];
+ if ((c.type & Chunk.TYPE_CORRUPT) != 0) {
+ Rectangle r = c.rect;
+ gc.drawRectangle(
+ baseX + getZoomedPixelSize(r.x),
+ baseY + getZoomedPixelSize(r.y),
+ getZoomedPixelSize(r.width),
+ getZoomedPixelSize(r.height));
+ }
+ }
+ }
+ }
+ }
+
+ private void drawGuideLine(GC gc, int baseX, int baseY) {
+ gc.setAntialias(SWT.ON);
+ gc.setInterpolation(SWT.HIGH);
+
+ int x = Math.round((mCursorPoint.x * ((float) mZoom / 100) + baseX)
+ + ((float) mZoom / 100 / 2));
+ int y = Math.round((mCursorPoint.y * ((float) mZoom / 100) + baseY)
+ + ((float) mZoom / 100 / 2));
+ gc.setAlpha(GUIDE_ALPHA);
+
+ Point size = getSize();
+ gc.drawLine(x, 0, x, size.y);
+ gc.drawLine(0, y, size.x, y);
+
+ gc.setAlpha(NONE_ALPHA);
+ }
+
+ @Override
+ public void keyPressed(KeyEvent event) {
+ int keycode = event.keyCode;
+ if (keycode == SWT.CTRL) {
+ isCtrlPressed = true;
+ }
+ if (keycode == SWT.SHIFT) {
+ isShiftPressed = true;
+ if (mStatusChangedListener != null) {
+ mStatusChangedListener.helpTextChanged(HELP_MESSAGE_KEY_TIPS2);
+ }
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent event) {
+ int keycode = event.keyCode;
+ if (keycode == SWT.CTRL) {
+ isCtrlPressed = false;
+ }
+ if (keycode == SWT.SHIFT) {
+ isShiftPressed = false;
+ if (mStatusChangedListener != null) {
+ mStatusChangedListener.helpTextChanged(HELP_MESSAGE_KEY_TIPS);
+ }
+ }
+ }
+
+ @Override
+ public void dispose() {
+ mBackgroundLayer.dispose();
+ super.dispose();
+ }
+
+ /**
+ * Listen image updated event.
+ */
+ public interface UpdateListener {
+ /**
+ * 9-patched image has been updated.
+ */
+ public void update(NinePatchedImage image);
+ }
+
+ /**
+ * Listen status changed event.
+ */
+ public interface StatusChangedListener {
+ /**
+ * Mouse cursor position has been changed.
+ */
+ public void cursorPositionChanged(int x, int y);
+
+ /**
+ * Help text has been changed.
+ */
+ public void helpTextChanged(String text);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java
new file mode 100644
index 000000000..71845c11c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.ui;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Main frame.
+ */
+public class MainFrame extends Composite implements ImageViewer.StatusChangedListener {
+
+ private final StatusPanel mStatusPanel;
+ private final ImageEditorPanel mImageEditorPanel;
+
+ public StatusPanel getStatusPanel() {
+ return mStatusPanel;
+ }
+
+ public ImageEditorPanel getImageEditorPanel() {
+ return mImageEditorPanel;
+ }
+
+ public MainFrame(Composite parent, int style) {
+ super(parent, style);
+ setLayout(new FormLayout());
+
+ mStatusPanel = new StatusPanel(this, SWT.NULL);
+
+ FormData bottom = new FormData();
+ bottom.bottom = new FormAttachment(100, 0);
+ bottom.left = new FormAttachment(0, 0);
+ bottom.right = new FormAttachment(100, 0);
+ mStatusPanel.setLayoutData(bottom);
+
+ mImageEditorPanel = new ImageEditorPanel(this, SWT.NULL);
+ mImageEditorPanel.getImageViewer().setStatusChangedListener(this);
+
+ mStatusPanel.setStatusChangedListener(mImageEditorPanel);
+
+ FormData top = new FormData();
+ top.top = new FormAttachment(0);
+ top.left = new FormAttachment(0);
+ top.right = new FormAttachment(100);
+ top.bottom = new FormAttachment(mStatusPanel);
+ mImageEditorPanel.setLayoutData(top);
+
+ addKeyListener(mStatusPanel);
+ addKeyListener(mImageEditorPanel.getImageViewer());
+ }
+
+ @Override
+ public void cursorPositionChanged(int x, int y) {
+ mStatusPanel.setPosition(x, y);
+ }
+
+ @Override
+ public void helpTextChanged(String text) {
+ mStatusPanel.setHelpText(text);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java
new file mode 100644
index 000000000..6ad258ee2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Scale;
+
+/**
+ * Status and control pane.
+ */
+public class StatusPanel extends Composite implements KeyListener {
+
+ public static final int SCALE_MIN = 2;
+ public static final int SCALE_MAX = 6;
+
+ public static final int ZOOM_MIN = 100;
+ public static final int ZOOM_MAX = 800;
+
+ public static final int PADDING_TOP = 12;
+ public static final int PADDING_RIGHT = 0;
+ public static final int PADDING_BOTTOM = 5;
+ public static final int PADDING_LEFT = 10;
+
+ public static final int MIN_WIDTH = 800;
+
+ private Button mShowLock = null;
+ private Button mShowPatches = null;
+ private Button mShowBadPatches = null;
+ private Button mShowContent = null;
+
+ private Label mHelpLabel = null;
+
+ private Label mXPosLabel = null;
+ private Label mYPosLabel = null;
+
+ private ZoomControl mZoomControl = null;
+ private ZoomControl mScaleControl = null;
+
+ private StatusChangedListener mListener = null;
+
+ public void setStatusChangedListener(StatusChangedListener l) {
+ mListener = l;
+ }
+
+ public void setHelpText(String text) {
+ Point size = getSize();
+ // check window width
+ if (MIN_WIDTH < size.x) {
+ mHelpLabel.setText(text);
+ mHelpLabel.setVisible(true);
+ } else {
+ mHelpLabel.setText("N/A");
+ mHelpLabel.setVisible(false);
+ }
+ }
+
+ /**
+ * Set mouse cursor position.
+ */
+ public void setPosition(int x, int y) {
+ mXPosLabel.setText(String.format("X: %4d px", x));
+ mYPosLabel.setText(String.format("Y: %4d px", y));
+ }
+
+ public StatusPanel(Composite parent, int style) {
+ super(parent, style);
+ setLayout(new FormLayout());
+
+ final Composite container = new Composite(this, SWT.NULL);
+ container.setLayout(new FormLayout());
+
+ FormData innerForm = new FormData();
+ innerForm.left = new FormAttachment(0, PADDING_LEFT);
+ innerForm.top = new FormAttachment(0, PADDING_TOP);
+ innerForm.right = new FormAttachment(100, PADDING_RIGHT);
+ innerForm.bottom = new FormAttachment(100, -PADDING_BOTTOM);
+ container.setLayoutData(innerForm);
+
+ buildPosition(container);
+
+ Composite zoomPanels = new Composite(container, SWT.NULL);
+ zoomPanels.setLayout(new GridLayout(3, false));
+
+ buildZoomControl(zoomPanels);
+ buildScaleControl(zoomPanels);
+
+ Composite checkPanel = new Composite(container, SWT.NULL);
+ checkPanel.setLayout(new GridLayout(2, false));
+ FormData checkPanelForm = new FormData();
+ checkPanelForm.left = new FormAttachment(zoomPanels, 0);
+ checkPanelForm.bottom = new FormAttachment(100, -PADDING_BOTTOM);
+ checkPanel.setLayoutData(checkPanelForm);
+
+ buildCheckboxes(checkPanel);
+
+ mHelpLabel = new Label(container, SWT.BORDER_SOLID | SWT.BOLD | SWT.WRAP);
+ mHelpLabel.setBackground(new Color(AdtPlugin.getDisplay(), 0xFF, 0xFF, 0xFF));
+ FormData hintForm = new FormData();
+ hintForm.left = new FormAttachment(checkPanel, 5);
+ hintForm.right = new FormAttachment(mXPosLabel, -10);
+ hintForm.top = new FormAttachment(PADDING_TOP);
+ hintForm.bottom = new FormAttachment(100, -PADDING_BOTTOM);
+ mHelpLabel.setLayoutData(hintForm);
+
+ /*
+ * If the window width is not much, the "help label" will break the window.
+ * Because that is wrapped automatically.
+ *
+ * This listener catch resized events and reset help text.
+ *
+ * setHelpText method checks window width.
+ * If window is too narrow, help text will be set invisible.
+ */
+ container.addControlListener(new ControlListener() {
+ @Override
+ public void controlResized(ControlEvent event) {
+ // reset text
+ setHelpText(ImageViewer.HELP_MESSAGE_KEY_TIPS);
+ }
+ @Override
+ public void controlMoved(ControlEvent event) {
+ }
+ });
+
+ }
+
+ private void buildPosition(Composite parent) {
+ mXPosLabel = new Label(parent, SWT.NULL);
+ mYPosLabel = new Label(parent, SWT.NULL);
+
+ mXPosLabel.setText(String.format("X: %4d px", 1000));
+ mYPosLabel.setText(String.format("Y: %4d px", 1000));
+
+ FormData bottomRight = new FormData();
+ bottomRight.bottom = new FormAttachment(100, 0);
+ bottomRight.right = new FormAttachment(100, 0);
+ mYPosLabel.setLayoutData(bottomRight);
+
+ FormData aboveYPosLabel = new FormData();
+ aboveYPosLabel.bottom = new FormAttachment(mYPosLabel);
+ aboveYPosLabel.right = new FormAttachment(100, 0);
+ mXPosLabel.setLayoutData(aboveYPosLabel);
+ }
+
+ private void buildScaleControl(Composite parent) {
+ mScaleControl = new ZoomControl(parent);
+ mScaleControl.maxLabel.setText("6x");
+ mScaleControl.minLabel.setText("2x");
+ mScaleControl.scale.setMinimum(SCALE_MIN);
+ mScaleControl.scale.setMaximum(SCALE_MAX);
+ mScaleControl.scale.setSelection(2);
+ mScaleControl.scale.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ Scale scale = (Scale) event.widget;
+ mListener.scaleChanged(scale.getSelection());
+ }
+ }
+ });
+ }
+
+ private void buildZoomControl(Composite parent) {
+ mZoomControl = new ZoomControl(parent);
+ mZoomControl.maxLabel.setText("800%");
+ mZoomControl.minLabel.setText("100%");
+ mZoomControl.scale.setMinimum(ZOOM_MIN);
+ mZoomControl.scale.setMaximum(ZOOM_MAX - ZOOM_MIN);
+ mZoomControl.scale.setSelection(400);
+ mZoomControl.scale.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ Scale scale = (Scale) event.widget;
+ mListener.zoomChanged(scale.getSelection() + ZOOM_MIN);
+ }
+ }
+ });
+
+ }
+
+ private void buildCheckboxes(Composite parent) {
+ // check lock
+ mShowLock = new Button(parent, SWT.CHECK);
+ mShowLock.setText("show Lock");
+ mShowLock.setSelection(true);
+ mShowLock.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.lockVisibilityChanged(mShowLock.getSelection());
+ }
+ }
+ });
+
+ // check patches
+ mShowPatches = new Button(parent, SWT.CHECK);
+ mShowPatches.setText("show Patches");
+ mShowPatches.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.patchesVisibilityChanged(mShowPatches.getSelection());
+ }
+ }
+ });
+
+ // check patches
+ mShowBadPatches = new Button(parent, SWT.CHECK);
+ mShowBadPatches.setText("show Bad patches");
+ mShowBadPatches.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.badPatchesVisibilityChanged(mShowBadPatches.getSelection());
+ }
+ }
+ });
+
+ // check contents(padding)
+ mShowContent = new Button(parent, SWT.CHECK);
+ mShowContent.setText("show Contents");
+ mShowContent.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.contentAreaVisibilityChanged(mShowContent.getSelection());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void keyPressed(KeyEvent event) {
+ switch (event.character) {
+ case 'c':
+ mShowContent.setSelection(!mShowContent.getSelection());
+ if (mListener != null) {
+ mListener.contentAreaVisibilityChanged(mShowContent.getSelection());
+ }
+ break;
+ case 'l':
+ mShowLock.setSelection(!mShowLock.getSelection());
+ if (mListener != null) {
+ mListener.lockVisibilityChanged(mShowLock.getSelection());
+ }
+ break;
+ case 'p':
+ mShowPatches.setSelection(!mShowPatches.getSelection());
+ if (mListener != null) {
+ mListener.patchesVisibilityChanged(mShowPatches.getSelection());
+ }
+ break;
+ case 'b':
+ mShowBadPatches.setSelection(!mShowBadPatches.getSelection());
+ if (mListener != null) {
+ mListener.badPatchesVisibilityChanged(mShowBadPatches.getSelection());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent event) {
+ }
+
+ private static class ZoomControl {
+
+ private Label minLabel;
+ private Label maxLabel;
+ Scale scale;
+
+ public ZoomControl(Composite composite) {
+ minLabel = new Label(composite, SWT.RIGHT);
+ scale = new Scale(composite, SWT.HORIZONTAL);
+ maxLabel = new Label(composite, SWT.LEFT);
+ }
+ }
+
+ /**
+ * Status changed events listener.
+ */
+ public interface StatusChangedListener {
+ /**
+ * Zoom level has been changed.
+ * @param zoom
+ */
+ public void zoomChanged(int zoom);
+
+ /**
+ * Scale has been changed.
+ * @param scale
+ */
+ public void scaleChanged(int scale);
+
+ /**
+ * Lock visibility has been changed.
+ * @param visible
+ */
+ public void lockVisibilityChanged(boolean visible);
+
+ /**
+ * Patches visibility has been changed.
+ * @param visible
+ */
+ public void patchesVisibilityChanged(boolean visible);
+
+ /**
+ * BadPatches visibility has been changed.
+ * @param visible
+ */
+ public void badPatchesVisibilityChanged(boolean visible);
+
+ /**
+ * Content visibility has been changed.
+ * @param visible
+ */
+ public void contentAreaVisibilityChanged(boolean visible);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java
new file mode 100644
index 000000000..353312c85
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.draw9patch.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.GraphicsUtilities;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage.Projection;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Preview 9-patched image pane.
+ */
+public class StretchesViewer extends Composite {
+ private static final boolean DEBUG = false;
+
+ private static final int PADDING_COLOR = 0x0000CC;
+
+ private static final int PADDING_COLOR_ALPHA = 100;
+
+ private static final PaletteData PADDING_PALLET
+ = new PaletteData(new RGB[] {new RGB(0x00, 0x00, 0xCC)});
+
+ private static final String CHECKER_PNG_PATH = "/icons/checker.png";
+
+ private Image mBackgroundLayer = null;
+
+ private final StretchView mHorizontal;
+ private final StretchView mVertical;
+
+ private final StretchView mBoth;
+
+ private NinePatchedImage mNinePatchedImage = null;
+
+ private ImageData mContentAreaImageData = null;
+
+ private boolean mIsContentAreaShown = false;
+
+ private Image mContentAreaImage = null;
+
+ private int mScale = 2;
+
+ public StretchesViewer(Composite parent, int style) {
+ super(parent, style);
+
+ mBackgroundLayer = AdtPlugin.getImageDescriptor(CHECKER_PNG_PATH).createImage();
+
+ setLayout(new FillLayout(SWT.VERTICAL));
+
+ mHorizontal = new StretchView(this, SWT.NULL);
+ mVertical = new StretchView(this, SWT.NULL);
+ mBoth = new StretchView(this, SWT.NULL);
+ }
+
+ /**
+ * Set show/not show content area.
+ * @param If show, true
+ */
+ public void setShowContentArea(boolean show) {
+ mIsContentAreaShown = show;
+ redraw();
+ }
+
+ private static final boolean equalSize(ImageData image1, ImageData image2) {
+ return (image1.width == image2.width && image1.height == image2.height);
+ }
+
+ /**
+ * Update preview image.
+ */
+ public void updatePreview(NinePatchedImage image) {
+ mNinePatchedImage = image;
+ ImageData base = mNinePatchedImage.getImageData();
+
+ if (mContentAreaImageData == null
+ || (mContentAreaImageData != null && !equalSize(base, mContentAreaImageData))) {
+ mContentAreaImageData = new ImageData(
+ base.width,
+ base.height,
+ 1,
+ PADDING_PALLET);
+ } else {
+ GraphicsUtilities.clearImageData(mContentAreaImageData);
+ }
+
+ mHorizontal.setImage(mNinePatchedImage);
+ mVertical.setImage(mNinePatchedImage);
+ mBoth.setImage(mNinePatchedImage);
+
+ mContentAreaImage = buildContentAreaPreview();
+
+ setScale(mScale);
+ }
+
+ private Image buildContentAreaPreview() {
+ if (mContentAreaImage != null) {
+ mContentAreaImage.dispose();
+ }
+
+ Rectangle rect = mNinePatchedImage.getContentArea();
+
+ int yLen = rect.y + rect.height;
+ for (int y = rect.y; y < yLen; y++) {
+ int xLen = rect.x + rect.width;
+ for (int x = rect.x; x < xLen; x++) {
+ mContentAreaImageData.setPixel(x, y, PADDING_COLOR);
+ mContentAreaImageData.setAlpha(x, y, PADDING_COLOR_ALPHA);
+ }
+ }
+ return new Image(AdtPlugin.getDisplay(), mContentAreaImageData);
+ }
+
+ public void setScale(int scale) {
+ if (DEBUG) {
+ System.out.println("scale = " + scale);
+ }
+
+ mScale = scale;
+ int imageWidth = mNinePatchedImage.getWidth();
+ int imageHeight = mNinePatchedImage.getHeight();
+
+ mHorizontal.setSize(imageWidth * scale, imageHeight);
+ mVertical.setSize(imageWidth, imageHeight * scale);
+ mBoth.setSize(imageWidth * scale, imageHeight * scale);
+
+ redraw();
+ }
+
+ @Override
+ public void dispose() {
+ mBackgroundLayer.dispose();
+ super.dispose();
+ }
+
+ private class StretchView extends Canvas implements PaintListener {
+
+ private final Point mSize = new Point(0, 0);
+ private final Rectangle mPadding = new Rectangle(0, 0, 0, 0);
+ private Projection[][] mProjection = null;
+
+ public StretchView(Composite parent, int style) {
+ super(parent, style);
+ addPaintListener(this);
+ }
+
+ private void setImage(NinePatchedImage image) {
+ setSize(image.getWidth(), image.getHeight());
+ }
+
+ @Override
+ public void setSize(int width, int heigh) {
+ mSize.x = width;
+ mSize.y = heigh;
+ mProjection = mNinePatchedImage.getProjections(mSize.x, mSize.y, mProjection);
+ }
+
+ private synchronized void calcPaddings(int width, int height) {
+ Point canvasSize = getSize();
+
+ mPadding.x = (canvasSize.x - width) / 2;
+ mPadding.y = (canvasSize.y - height) / 2;
+
+ mPadding.width = width;
+ mPadding.height = height;
+ }
+
+ @Override
+ public void paintControl(PaintEvent pe) {
+ if (mNinePatchedImage == null || mProjection == null) {
+ return;
+ }
+
+ Point size = getSize();
+
+ // relative scaling
+ float ratio = 1.0f;
+ float wRatio = ((float) size.x / mSize.x);
+ ratio = Math.min(wRatio, ratio);
+ float hRatio = ((float) size.y / mSize.y);
+ ratio = Math.min(hRatio, ratio);
+
+ int width = Math.round(mSize.x * ratio);
+ int height = Math.round(mSize.y * ratio);
+
+ calcPaddings(width, height);
+
+ Rectangle dest = new Rectangle(0, 0, 0, 0);
+
+ GC gc = pe.gc;
+
+ int backgroundLayerWidth = mBackgroundLayer.getImageData().width;
+ int backgroundLayerHeight = mBackgroundLayer.getImageData().height;
+
+ int yCount = size.y / backgroundLayerHeight
+ + ((size.y % backgroundLayerHeight) > 0 ? 1 : 0);
+ int xCount = size.x / backgroundLayerWidth
+ + ((size.x % backgroundLayerWidth) > 0 ? 1 : 0);
+
+ // draw background layer
+ for (int y = 0; y < yCount; y++) {
+ for (int x = 0; x < xCount; x++) {
+ gc.drawImage(mBackgroundLayer,
+ x * backgroundLayerWidth,
+ y * backgroundLayerHeight);
+ }
+ }
+
+ // draw the border line
+ gc.setAlpha(0x88);
+ gc.drawRectangle(0, 0, size.x, size.y);
+ gc.setAlpha(0xFF);
+
+ int yLen = mProjection.length;
+ int xLen = mProjection[0].length;
+ for (int yPos = 0; yPos < yLen; yPos++) {
+ for (int xPos = 0; xPos < xLen; xPos++) {
+ Projection p = mProjection[yPos][xPos];
+
+ // consider the scale
+ dest.x = (int) Math.ceil(p.dest.x * ratio);
+ dest.y = (int) Math.ceil(p.dest.y * ratio);
+ dest.width = (int) Math.ceil(p.dest.width * ratio);
+ dest.height = (int) Math.ceil(p.dest.height * ratio);
+
+ gc.drawImage(mNinePatchedImage.getImage(), p.src.x, p.src.y,
+ p.src.width, p.src.height,
+ (mPadding.x + dest.x), (mPadding.y + dest.y),
+ dest.width, dest.height);
+
+ if (mIsContentAreaShown) {
+ gc.drawImage(mContentAreaImage, p.src.x, p.src.y,
+ p.src.width, p.src.height,
+ (mPadding.x + dest.x), (mPadding.y + dest.y),
+ dest.width, dest.height);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableContentAssist.java
new file mode 100644
index 000000000..b0bea511d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableContentAssist.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.drawable;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+/**
+ * Content Assist Processor for /res/drawable XML files
+ */
+@VisibleForTesting
+public final class DrawableContentAssist extends AndroidContentAssist {
+ public DrawableContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_DRAWABLE);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableDescriptors.java
new file mode 100644
index 000000000..4858ac7c6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableDescriptors.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.drawable;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.eclipse.adt.internal.editors.animator.AnimatorDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.resources.ResourceType;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Descriptors for /res/drawable files
+ */
+public class DrawableDescriptors implements IDescriptorProvider {
+ private static final String SDK_URL_BASE =
+ "http://d.android.com/guide/topics/resources/"; //$NON-NLS-1$
+
+ /** The root element descriptor */
+ private ElementDescriptor mDescriptor;
+ /** The root element descriptors */
+ private ElementDescriptor[] mRootDescriptors;
+ private Map<String, ElementDescriptor> nameToDescriptor;
+
+ /** @return the root descriptor. */
+ @Override
+ public ElementDescriptor getDescriptor() {
+ if (mDescriptor == null) {
+ mDescriptor = new ElementDescriptor("", getRootElementDescriptors()); //$NON-NLS-1$
+ }
+
+ return mDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mRootDescriptors;
+ }
+
+ /**
+ * Returns a descriptor for the given root tag name
+ *
+ * @param tag the tag name to look up a descriptor for
+ * @return a descriptor with the given tag name
+ */
+ public ElementDescriptor getElementDescriptor(String tag) {
+ if (nameToDescriptor == null) {
+ nameToDescriptor = new HashMap<String, ElementDescriptor>();
+ for (ElementDescriptor descriptor : getRootElementDescriptors()) {
+ nameToDescriptor.put(descriptor.getXmlName(), descriptor);
+ }
+ }
+
+ ElementDescriptor descriptor = nameToDescriptor.get(tag);
+ if (descriptor == null) {
+ descriptor = getDescriptor();
+ }
+ return descriptor;
+ }
+
+ public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) {
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(ANDROID_NS_NAME,
+ ANDROID_URI);
+ List<ElementDescriptor> descriptors = new ArrayList<ElementDescriptor>();
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "animation-list", "Animation List", "AnimationDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An animation defined in XML that shows a sequence of images in "
+ + "order (like a film)",
+ SDK_URL_BASE + "animation-resource.html#Frame",
+ xmlns, null, true /*mandatory*/);
+
+ /* For some reason, android.graphics.drawable.AnimatedRotateDrawable is marked with @hide
+ * so we should not register it and its attributes here. See issue #19248 for details.
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "animated-rotate", "Animated Rotate", "AnimatedRotateDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ // Need docs
+ null, // tooltip
+ null, // sdk_url
+ xmlns, null, true);
+ */
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "bitmap", "BitMap", "BitmapDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML bitmap is a resource defined in XML that points to a bitmap file. "
+ + "The effect is an alias for a raw bitmap file. The XML can "
+ + "specify additional properties for the bitmap such as "
+ + "dithering and tiling.",
+ SDK_URL_BASE + "drawable-resource.html#Bitmap", //$NON-NLS-1$
+ xmlns, null, true /* mandatory */);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "clip", "Clip", "ClipDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML file that defines a drawable that clips another Drawable based on "
+ + "this Drawable's current level value.",
+ SDK_URL_BASE + "drawable-resource.html#Clip", //$NON-NLS-1$
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "color", "Color", "ColorDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "XML resource that carries a color value (a hexadecimal color)",
+ SDK_URL_BASE + "more-resources.html#Color",
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "inset", "Inset", "InsetDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML file that defines a drawable that insets another drawable by a "
+ + "specified distance. This is useful when a View needs a background "
+ + "drawble that is smaller than the View's actual bounds.",
+ SDK_URL_BASE + "drawable-resource.html#Inset", //$NON-NLS-1$
+ xmlns, null, true /*mandatory*/);
+
+ // Layer list
+
+ // An <item> in a <selector> or <
+ ElementDescriptor layerItem = AnimatorDescriptors.addElement(null, styleMap,
+ "item", "Item", "LayerDrawableItem", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "Defines a drawable to place in the layer drawable, in a position "
+ + "defined by its attributes. Must be a child of a <selector> "
+ + "element. Accepts child <bitmap> elements.",
+ SDK_URL_BASE + "drawable-resource.html#LayerList", //$NON-NLS-1$
+ null, /* extra attribute */
+ null, /* This is wrong -- we can now embed any above drawable
+ (but without xmlns as extra) */
+ false /*mandatory*/);
+ ElementDescriptor[] layerChildren = layerItem != null
+ ? new ElementDescriptor[] { layerItem } : null;
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "layer-list", "Layer List", "LayerDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "A Drawable that manages an array of other Drawables. These are drawn in "
+ + "array order, so the element with the largest index is be drawn on top.",
+ SDK_URL_BASE + "drawable-resource.html#LayerList", //$NON-NLS-1$
+ xmlns,
+ layerChildren,
+ true /*mandatory*/);
+
+ // Level list children
+ ElementDescriptor levelListItem = AnimatorDescriptors.addElement(null, styleMap,
+ "item", "Item", "LevelListDrawableItem", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "Defines a drawable to use at a certain level.",
+ SDK_URL_BASE + "drawable-resource.html#LevelList", //$NON-NLS-1$
+ null, /* extra attribute */
+ null, /* no further children */
+ // TODO: The inflation code seems to show that all drawables can be nested here!
+ false /*mandatory*/);
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "level-list", "Level List", "LevelListDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML file that defines a drawable that manages a number of alternate "
+ + "Drawables, each assigned a maximum numerical value",
+ SDK_URL_BASE + "drawable-resource.html#LevelList", //$NON-NLS-1$
+ xmlns,
+ levelListItem != null ? new ElementDescriptor[] { levelListItem } : null,
+ true /*mandatory*/);
+
+ // Not yet supported
+ //addElement(descriptors, styleMap, "mipmap", "Mipmap", "MipmapDrawable", null,
+ // null /* tooltip */,
+ // null /* sdk_url */,
+ // xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "nine-patch", "Nine Patch", "NinePatchDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "A PNG file with stretchable regions to allow image resizing "
+ + "based on content (.9.png).",
+ SDK_URL_BASE + "drawable-resource.html#NinePatch", //$NON-NLS-1$
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "rotate", "Rotate", "RotateDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ // Need docs
+ null /* tooltip */,
+ null /* sdk_url */,
+ xmlns, null, true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "scale", "Shape", "ScaleDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML file that defines a drawable that changes the size of another Drawable "
+ + "based on its current level value.",
+ SDK_URL_BASE + "drawable-resource.html#Scale", //$NON-NLS-1$
+ xmlns, null, true /*mandatory*/);
+
+ // Selector children
+ ElementDescriptor selectorItem = AnimatorDescriptors.addElement(null, styleMap,
+ "item", "Item", "DrawableStates", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "Defines a drawable to use during certain states, as described by "
+ + "its attributes. Must be a child of a <selector> element.",
+ SDK_URL_BASE + "drawable-resource.html#StateList", //$NON-NLS-1$
+ new ReferenceAttributeDescriptor(
+ ResourceType.DRAWABLE, "drawable", ANDROID_URI, //$NON-NLS-1$
+ new AttributeInfo("drawable", Format.REFERENCE_SET))
+ .setTooltip("Reference to a drawable resource."),
+ null, /* This is wrong -- we can now embed any above drawable
+ (but without xmlns as extra) */
+ false /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "selector", "Selector", "StateListDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML file that references different bitmap graphics for different states "
+ + "(for example, to use a different image when a button is pressed).",
+ SDK_URL_BASE + "drawable-resource.html#StateList", //$NON-NLS-1$
+ xmlns,
+ selectorItem != null ? new ElementDescriptor[] { selectorItem } : null,
+ true /*mandatory*/);
+
+ // Shape
+ // Shape children
+ List<ElementDescriptor> shapeChildren = new ArrayList<ElementDescriptor>();
+ // Selector children
+ AnimatorDescriptors.addElement(shapeChildren, styleMap,
+ "size", "Size", "GradientDrawableSize", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, null /* sdk_url */, null /* extra attribute */,
+ null /* children */, false /* mandatory */);
+ AnimatorDescriptors.addElement(shapeChildren, styleMap,
+ "gradient", "Gradient", "GradientDrawableGradient", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, null /* sdk_url */, null /* extra attribute */,
+ null /* children */, false /* mandatory */);
+ AnimatorDescriptors.addElement(shapeChildren, styleMap,
+ "solid", "Solid", "GradientDrawableSolid", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, null /* sdk_url */, null /* extra attribute */,
+ null /* children */, false /* mandatory */);
+ AnimatorDescriptors.addElement(shapeChildren, styleMap,
+ "stroke", "Stroke", "GradientDrawableStroke", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, null /* sdk_url */, null /* extra attribute */,
+ null /* children */, false /* mandatory */);
+ AnimatorDescriptors.addElement(shapeChildren, styleMap,
+ "corners", "Corners", "DrawableCorners", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, null /* sdk_url */, null /* extra attribute */,
+ null /* children */, false /* mandatory */);
+ AnimatorDescriptors.addElement(shapeChildren, styleMap,
+ "padding", "Padding", "GradientDrawablePadding", null, //$NON-NLS-1$ //$NON-NLS-3$
+ null /* tooltip */, null /* sdk_url */, null /* extra attribute */,
+ null /* children */, false /* mandatory */);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "shape", "Shape", //$NON-NLS-1$
+
+ // The documentation says that a <shape> element creates a ShapeDrawable,
+ // but ShapeDrawable isn't finished and the code currently creates
+ // a GradientDrawable.
+ //"ShapeDrawable", //$NON-NLS-1$
+ "GradientDrawable", //$NON-NLS-1$
+
+ null,
+ "An XML file that defines a geometric shape, including colors and gradients.",
+ SDK_URL_BASE + "drawable-resource.html#Shape", //$NON-NLS-1$
+ xmlns,
+
+ // These are the GradientDrawable children, not the ShapeDrawable children
+ shapeChildren.toArray(new ElementDescriptor[shapeChildren.size()]),
+ true /*mandatory*/);
+
+ AnimatorDescriptors.addElement(descriptors, styleMap,
+ "transition", "Transition", "TransitionDrawable", null, //$NON-NLS-1$ //$NON-NLS-3$
+ "An XML file that defines a drawable that can cross-fade between two "
+ + "drawable resources. Each drawable is represented by an <item> "
+ + "element inside a single <transition> element. No more than two "
+ + "items are supported. To transition forward, call startTransition(). "
+ + "To transition backward, call reverseTransition().",
+ SDK_URL_BASE + "drawable-resource.html#Transition", //$NON-NLS-1$
+ xmlns,
+ layerChildren, // children: a TransitionDrawable is a LayerDrawable
+ true /*mandatory*/);
+
+ mRootDescriptors = descriptors.toArray(new ElementDescriptor[descriptors.size()]);
+
+ // A <selector><item> can contain any of the top level drawables
+ if (selectorItem != null) {
+ selectorItem.setChildren(mRootDescriptors);
+ }
+ // Docs says it can accept <bitmap> but code comment suggests any is possible;
+ // test and either use this or just { bitmap }
+ if (layerItem != null) {
+ layerItem.setChildren(mRootDescriptors);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableEditorDelegate.java
new file mode 100644
index 000000000..a54fa8c41
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/drawable/DrawableEditorDelegate.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.drawable;
+
+import static com.android.ide.eclipse.adt.AdtConstants.EDITORS_NAMESPACE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Editor for /res/drawable XML files.
+ */
+@SuppressWarnings("restriction")
+public class DrawableEditorDelegate extends CommonXmlDelegate {
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public DrawableEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.DRAWABLE == type) {
+ return new DrawableEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ EDITORS_NAMESPACE + ".drawable.DrawableEditor"; //$NON-NLS-1$
+
+ /** The tag used at the root */
+ private String mRootTag;
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ private DrawableEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new DrawableContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ @Override
+ public void delegateCreateFormPages() {
+ /* Disabled for now; doesn't work quite right
+ try {
+ addPage(new DrawableTreePage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(IStatus.ERROR, "Error creating nested page"); //$NON-NLS-1$
+ AdtPlugin.getDefault().getLog().log(e.getStatus());
+ }
+ */
+ }
+
+ @Override
+ public void delegateXmlModelChanged(Document xmlDoc) {
+ Element rootElement = xmlDoc.getDocumentElement();
+ if (rootElement != null) {
+ mRootTag = rootElement.getTagName();
+ }
+
+ delegateInitUiRootNode(false /*force*/);
+
+ if (mRootTag != null
+ && !mRootTag.equals(getUiRootNode().getDescriptor().getXmlLocalName())) {
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data != null) {
+ ElementDescriptor descriptor =
+ data.getDrawableDescriptors().getElementDescriptor(mRootTag);
+ // Replace top level node now that we know the actual type
+
+ // Disconnect from old
+ getUiRootNode().setEditor(null);
+ getUiRootNode().setXmlDocument(null);
+
+ // Create new
+ setUiRootNode(descriptor.createUiNode());
+ getUiRootNode().setXmlDocument(xmlDoc);
+ getUiRootNode().setEditor(getEditor());
+ }
+ }
+
+ if (getUiRootNode().getDescriptor() instanceof DocumentDescriptor) {
+ getUiRootNode().loadFromXmlNode(xmlDoc);
+ } else {
+ getUiRootNode().loadFromXmlNode(rootElement);
+ }
+ }
+
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (getUiRootNode() == null || force) {
+ ElementDescriptor descriptor;
+ boolean reload = false;
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data == null) {
+ descriptor = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ descriptor = data.getDrawableDescriptors().getElementDescriptor(mRootTag);
+ reload = true;
+ }
+ setUiRootNode(descriptor.createUiNode());
+ getUiRootNode().setEditor(getEditor());
+
+ if (reload) {
+ onDescriptorsChanged();
+ }
+ }
+ }
+
+ private void onDescriptorsChanged() {
+ IStructuredModel model = getEditor().getModelForRead();
+ if (model != null) {
+ try {
+ Node node = getEditor().getXmlDocument(model).getDocumentElement();
+ getUiRootNode().reloadFromXmlNode(node);
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java
new file mode 100755
index 000000000..bc3051e88
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.export;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * Section part for editing fields of a properties file in an Export editor.
+ * <p/>
+ * This base class is intended to be derived and customized.
+ */
+abstract class AbstractPropertiesFieldsPart extends ManifestSectionPart {
+
+ private final HashMap<String, Control> mNameToField = new HashMap<String, Control>();
+
+ private ExportEditor mEditor;
+
+ private boolean mInternalTextUpdate = false;
+
+ public AbstractPropertiesFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) {
+ super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+ mEditor = editor;
+ }
+
+ protected HashMap<String, Control> getNameToField() {
+ return mNameToField;
+ }
+
+ protected ExportEditor getEditor() {
+ return mEditor;
+ }
+
+ protected void setInternalTextUpdate(boolean internalTextUpdate) {
+ mInternalTextUpdate = internalTextUpdate;
+ }
+
+ protected boolean isInternalTextUpdate() {
+ return mInternalTextUpdate;
+ }
+
+ /**
+ * Adds a modify listener to every text field that will mark the part as dirty.
+ *
+ * CONTRACT: Derived classes MUST call this at the end of their constructor.
+ *
+ * @see #setFieldModifyListener(Control, ModifyListener)
+ */
+ protected void addModifyListenerToFields() {
+ ModifyListener markDirtyListener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ // Mark the part as dirty if a field has been changed.
+ // This will force a commit() operation to store the data in the model.
+ if (!mInternalTextUpdate) {
+ markDirty();
+ }
+ }
+ };
+
+ for (Control field : mNameToField.values()) {
+ setFieldModifyListener(field, markDirtyListener);
+ }
+ }
+
+ /**
+ * Sets a listener that will mark the part as dirty when the control is modified.
+ * The base method only handles {@link Text} fields.
+ *
+ * CONTRACT: Derived classes CAN use this to add a listener to their own controls.
+ * The listener must call {@link #markDirty()} when the control is modified by the user.
+ *
+ * @param field A control previously registered with {@link #getNameToField()}.
+ * @param markDirtyListener A {@link ModifyListener} that invokes {@link #markDirty()}.
+ *
+ * @see #isInternalTextUpdate()
+ */
+ protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) {
+ if (field instanceof Text) {
+ ((Text) field).addModifyListener(markDirtyListener);
+ }
+ }
+
+ /**
+ * Updates the model based on the content of fields. This is invoked when a field
+ * has marked the document as dirty.
+ *
+ * CONTRACT: Derived classes do not need to override this.
+ */
+ @Override
+ public void commit(boolean onSave) {
+
+ // We didn't store any information indicating which field was dirty (we could).
+ // Since there are not many fields, just update all the document lines that
+ // match our field keywords.
+
+ if (isDirty()) {
+ mEditor.wrapRewriteSession(new Runnable() {
+ @Override
+ public void run() {
+ saveFieldsToModel();
+ }
+ });
+ }
+
+ super.commit(onSave);
+ }
+
+ private void saveFieldsToModel() {
+ // Get a list of all keywords to process. Go thru the document, replacing in-place
+ // the ones we can find and remove them from this set. This will leave the list
+ // of new keywords to add at the end of the document.
+ HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet());
+
+ IDocument doc = mEditor.getDocument();
+ int numLines = doc.getNumberOfLines();
+
+ String delim = null;
+ try {
+ delim = numLines > 0 ? doc.getLineDelimiter(0) : null;
+ } catch (BadLocationException e1) {
+ // ignore
+ }
+ if (delim == null || delim.length() == 0) {
+ delim = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS ?
+ "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-2#
+ }
+
+ for (int i = 0; i < numLines; i++) {
+ try {
+ IRegion info = doc.getLineInformation(i);
+ String line = doc.get(info.getOffset(), info.getLength());
+ line = line.trim();
+ if (line.startsWith("#")) { //$NON-NLS-1$
+ continue;
+ }
+
+ int pos = line.indexOf('=');
+ if (pos > 0 && pos < line.length() - 1) {
+ String key = line.substring(0, pos).trim();
+
+ Control field = mNameToField.get(key);
+ if (field != null) {
+
+ // This is the new line to inject
+ line = key + "=" + getFieldText(field);
+
+ try {
+ // replace old line by new one. This doesn't change the
+ // line delimiter.
+ mInternalTextUpdate = true;
+ doc.replace(info.getOffset(), info.getLength(), line);
+ allKeywords.remove(key);
+ } finally {
+ mInternalTextUpdate = false;
+ }
+ }
+ }
+
+ } catch (BadLocationException e) {
+ // TODO log it
+ AdtPlugin.log(e, "Failed to replace in export.properties");
+ }
+ }
+
+ for (String key : allKeywords) {
+ Control field = mNameToField.get(key);
+ if (field != null) {
+ // This is the new line to inject
+ String line = key + "=" + getFieldText(field);
+
+ try {
+ // replace old line by new one
+ mInternalTextUpdate = true;
+
+ numLines = doc.getNumberOfLines();
+
+ IRegion info = numLines > 0 ? doc.getLineInformation(numLines - 1) : null;
+ if (info != null && info.getLength() == 0) {
+ // last line is empty. Insert right before there.
+ doc.replace(info.getOffset(), info.getLength(), line);
+ } else {
+ if (numLines > 0) {
+ String eofDelim = doc.getLineDelimiter(numLines - 1);
+ if (eofDelim == null || eofDelim.length() == 0) {
+ // The document doesn't end with a line delimiter, so add
+ // one to the line to be written.
+ line = delim + line;
+ }
+ }
+
+ int len = doc.getLength();
+ doc.replace(len, 0, line);
+ }
+
+ allKeywords.remove(key);
+ } catch (BadLocationException e) {
+ // TODO log it
+ AdtPlugin.log(e, "Failed to append to export.properties: %s", line);
+ } finally {
+ mInternalTextUpdate = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Used when committing fields values to the model to retrieve the text
+ * associated with a field.
+ * <p/>
+ * The base method only handles {@link Text} controls.
+ *
+ * CONTRACT: Derived classes CAN use this to support their own controls.
+ *
+ * @param field A control previously registered with {@link #getNameToField()}.
+ * @return A non-null string to write to the properties files.
+ */
+ protected String getFieldText(Control field) {
+ if (field instanceof Text) {
+ return ((Text) field).getText();
+ }
+ return "";
+ }
+
+ /**
+ * Called after all pages have been created, to let the parts initialize their
+ * content based on the document's model.
+ * <p/>
+ * The model should be acceded via the {@link ExportEditor}.
+ *
+ * @param editor The {@link ExportEditor} instance.
+ */
+ public void onModelInit(ExportEditor editor) {
+
+ // Start with a set of all the possible keywords and remove those we
+ // found in the document as we read the lines.
+ HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet());
+
+ // Parse the lines in the document for patterns "keyword=value",
+ // trimming all whitespace and discarding lines that start with # (comments)
+ // then affect to the internal fields as appropriate.
+ IDocument doc = editor.getDocument();
+ int numLines = doc.getNumberOfLines();
+ for (int i = 0; i < numLines; i++) {
+ try {
+ IRegion info = doc.getLineInformation(i);
+ String line = doc.get(info.getOffset(), info.getLength());
+ line = line.trim();
+ if (line.startsWith("#")) { //$NON-NLS-1$
+ continue;
+ }
+
+ int pos = line.indexOf('=');
+ if (pos > 0 && pos < line.length() - 1) {
+ String key = line.substring(0, pos).trim();
+
+ Control field = mNameToField.get(key);
+ if (field != null) {
+ String value = line.substring(pos + 1).trim();
+ try {
+ mInternalTextUpdate = true;
+ setFieldText(field, value);
+ allKeywords.remove(key);
+ } finally {
+ mInternalTextUpdate = false;
+ }
+ }
+ }
+
+ } catch (BadLocationException e) {
+ // TODO log it
+ AdtPlugin.log(e, "Failed to set field to export.properties value");
+ }
+ }
+
+ // Clear the text of any keyword we didn't find in the document
+ Iterator<String> iterator = allKeywords.iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ Control field = mNameToField.get(key);
+ if (field != null) {
+ try {
+ mInternalTextUpdate = true;
+ setFieldText(field, "");
+ iterator.remove();
+ } finally {
+ mInternalTextUpdate = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Used when reading the model to set the field values.
+ * <p/>
+ * The base method only handles {@link Text} controls.
+ *
+ * CONTRACT: Derived classes CAN use this to support their own controls.
+ *
+ * @param field A control previously registered with {@link #getNameToField()}.
+ * @param value A non-null string to that was read from the properties files.
+ * The value is an empty string if the property line is missing.
+ */
+ protected void setFieldText(Control field, String value) {
+ if (field instanceof Text) {
+ ((Text) field).setText(value);
+ }
+ }
+
+ /**
+ * Called after the document model has been changed. The model should be acceded via
+ * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession)
+ *
+ * @param editor The {@link ExportEditor} instance.
+ * @param event Specification of changes applied to document.
+ */
+ public void onModelChanged(ExportEditor editor, DocumentEvent event) {
+ // To simplify and since we don't have many fields, just reload all the values.
+ // A better way would to be to look at DocumentEvent which gives us the offset/length
+ // and text that has changed.
+ if (!mInternalTextUpdate) {
+ onModelInit(editor);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java
new file mode 100755
index 000000000..769f74e27
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.export;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.editors.AndroidTextEditor;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Multi-page form editor for export.properties in Export Projects.
+ */
+public class ExportEditor extends AndroidTextEditor {
+
+ public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".text.ExportEditor"; //$NON-NLS-1$
+
+ private ExportPropertiesPage mExportPropsPage;
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public ExportEditor() {
+ super();
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ mExportPropsPage = new ExportPropertiesPage(this);
+ addPage(mExportPropsPage);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the project name.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ setPartName(String.format("%1$s", file.getName()));
+ }
+ }
+
+ @Override
+ protected void postCreatePages() {
+ super.postCreatePages();
+ mExportPropsPage.onModelInit();
+ }
+
+ /**
+ * Indicates changes were made to the document.
+ *
+ * @param event Specification of changes applied to document.
+ */
+ @Override
+ protected void onDocumentChanged(DocumentEvent event) {
+ super.onDocumentChanged(event);
+ mExportPropsPage.onModelChanged(event);
+ }
+
+ // ---- Local Methods ----
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java
new file mode 100755
index 000000000..eff3e4806
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.export;
+
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.HashMap;
+
+/**
+ * Section part for editing the properties in an Export editor.
+ */
+final class ExportFieldsPart extends AbstractPropertiesFieldsPart {
+
+ public ExportFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) {
+ super(body, toolkit, editor);
+ Section section = getSection();
+
+ section.setText("Export Properties");
+ section.setDescription("Properties of export.properties:");
+
+ Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+ createLabel(table, toolkit,
+ "Available Properties", //label
+ "List of properties you can edit in export.properties"); //tooltip
+
+ Text packageField = createLabelAndText(table, toolkit,
+ "Package", //label,
+ "", //$NON-NLS-1$ value,
+ "TODO tooltip for Package"); //tooltip
+
+ Text projectsField = createLabelAndText(table, toolkit,
+ "Projects", //label,
+ "", //$NON-NLS-1$ value,
+ "TODO tooltip for Projects"); //tooltip
+
+ Text versionCodeField = createLabelAndText(table, toolkit,
+ "Version Code", //label,
+ "", //$NON-NLS-1$ value,
+ "TODO tooltip for Version Code"); //tooltip
+
+ Text keyStoreField = createLabelAndText(table, toolkit,
+ "Key Store", //label,
+ "", //$NON-NLS-1$ value,
+ "TODO tooltip for Key Store"); //tooltip
+
+ Text keyAliasField = createLabelAndText(table, toolkit,
+ "Key Alias", //label,
+ "", //$NON-NLS-1$ value,
+ "TODO tooltip for Key Alias"); //tooltip
+
+ // Associate each field with the keyword in the properties files.
+ // TODO there's probably some constant to reuse here.
+ HashMap<String, Control> map = getNameToField();
+ map.put("package", packageField); //$NON-NLS-1$
+ map.put("projects", projectsField); //$NON-NLS-1$
+ map.put("versionCode", versionCodeField); //$NON-NLS-1$
+ map.put("_key.store", keyStoreField); //$NON-NLS-1$
+ map.put("_key.alias", keyAliasField); //$NON-NLS-1$
+
+ addModifyListenerToFields();
+ }
+
+ @Override
+ protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) {
+ super.setFieldModifyListener(field, markDirtyListener);
+ // TODO override for custom controls
+ }
+
+ @Override
+ protected String getFieldText(Control field) {
+ // TODO override for custom controls
+ return super.getFieldText(field);
+ }
+
+ @Override
+ protected void setFieldText(Control field, String value) {
+ // TODO override for custom controls
+ super.setFieldText(field, value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java
new file mode 100644
index 000000000..85a2165cc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.export;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.MessageBox;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IHyperlinkListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Links section part for export properties page.
+ * Displays some help and some links/actions for the user to use.
+ */
+final class ExportLinksPart extends ManifestSectionPart {
+
+ private FormText mFormText;
+
+ public ExportLinksPart(Composite body, FormToolkit toolkit, ExportEditor editor) {
+ super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+ Section section = getSection();
+ section.setText("Links");
+ section.setDescription("TODO SOME TEXT HERE. You can also edit the XML directly.");
+
+ final Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+ StringBuffer buf = new StringBuffer();
+ buf.append("<form>"); //$NON-NLS-1$
+
+ buf.append("<li style=\"image\" value=\"android_img\"><a href=\"action_dosomething\">");
+ buf.append("TODO Custom Action");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": blah blah do something (like build/export).");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"android_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$
+ ExportEditor.TEXT_EDITOR_ID));
+ buf.append("XML Source");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Directly edit the AndroidManifest.xml file.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append("<li style=\"image\" value=\"android_img\">"); //$NON-NLS-1$
+ buf.append("<a href=\"http://code.google.com/android/devel/bblocks-manifest.html\">Documentation</a>: Documentation from the Android SDK for AndroidManifest.xml."); //$NON-NLS-1$
+ buf.append("</li>"); //$NON-NLS-1$
+ buf.append("</form>"); //$NON-NLS-1$
+
+ mFormText = createFormText(table, toolkit, true, buf.toString(),
+ false /* setupLayoutData */);
+
+ Image androidLogo = AdtPlugin.getAndroidLogo();
+ mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$
+
+ // Listener for default actions (page change, URL web browser)
+ mFormText.addHyperlinkListener(editor.createHyperlinkListener());
+
+ mFormText.addHyperlinkListener(new IHyperlinkListener() {
+ @Override
+ public void linkExited(HyperlinkEvent e) {
+ // pass
+ }
+
+ @Override
+ public void linkEntered(HyperlinkEvent e) {
+ // pass
+ }
+
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ String link = e.data.toString();
+ if ("action_dosomething".equals(link)) {
+ MessageBox mb = new MessageBox(table.getShell(), SWT.OK);
+ mb.setText("Custom Action Invoked");
+ mb.open();
+ }
+ }
+ });
+ }
+
+ /**
+ * Called after all pages have been created, to let the parts initialize their
+ * content based on the document's model.
+ * <p/>
+ * The model should be acceded via the {@link ExportEditor}.
+ *
+ * @param editor The {@link ExportEditor} instance.
+ */
+ public void onModelInit(ExportEditor editor) {
+ // pass
+ }
+
+ /**
+ * Called after the document model has been changed. The model should be acceded via
+ * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession)
+ *
+ * @param editor The {@link ExportEditor} instance.
+ * @param event Specification of changes applied to document.
+ */
+ public void onModelChanged(ExportEditor editor, DocumentEvent event) {
+ // pass
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java
new file mode 100755
index 000000000..f3db5eea6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.export;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ColumnLayout;
+import org.eclipse.ui.forms.widgets.ColumnLayoutData;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+
+/**
+ * Page for export properties, used by {@link ExportEditor}.
+ * It displays a part to edit the properties and another part
+ * to provide some links and actions.
+ */
+public final class ExportPropertiesPage extends FormPage {
+
+ /** Page id used for switching tabs programmatically */
+ final static String PAGE_ID = "export_prop_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ExportEditor mEditor;
+ /** Export fields part */
+ private ExportFieldsPart mFieldsPart;
+ /** Export links part */
+ private ExportLinksPart mLinksPart;
+
+ public ExportPropertiesPage(ExportEditor editor) {
+ super(editor, PAGE_ID, "Export Properties"); // tab's label, user visible, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Export Properties");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ Composite body = form.getBody();
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ body.setLayout(new ColumnLayout());
+
+ mFieldsPart = new ExportFieldsPart(body, toolkit, mEditor);
+ mFieldsPart.getSection().setLayoutData(new ColumnLayoutData());
+ managedForm.addPart(mFieldsPart);
+
+ mLinksPart = new ExportLinksPart(body, toolkit, mEditor);
+ mLinksPart.getSection().setLayoutData(new ColumnLayoutData());
+ managedForm.addPart(mLinksPart);
+
+ mFieldsPart.onModelInit(mEditor);
+ mLinksPart.onModelInit(mEditor);
+ }
+
+ /**
+ * Called after all pages have been created, to let the parts initialize their
+ * content based on the document's model.
+ * <p/>
+ * The model should be acceded via the {@link ExportEditor}.
+ */
+ public void onModelInit() {
+ if (mFieldsPart != null) {
+ mFieldsPart.onModelInit(mEditor);
+ }
+
+ if (mLinksPart != null) {
+ mLinksPart.onModelInit(mEditor);
+ }
+ }
+
+ /**
+ * Called after the document model has been changed. The model should be acceded via
+ * the {@link ExportEditor}.
+ *
+ * @param event Specification of changes applied to document.
+ */
+ public void onModelChanged(DocumentEvent event) {
+ if (mFieldsPart != null) {
+ mFieldsPart.onModelChanged(mEditor, event);
+ }
+
+ if (mLinksPart != null) {
+ mLinksPart.onModelChanged(mEditor, event);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormatter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormatter.java
new file mode 100644
index 000000000..403095450
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormatter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.formatting;
+
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.TypedPosition;
+import org.eclipse.jface.text.formatter.FormattingContext;
+import org.eclipse.jface.text.formatter.FormattingContextProperties;
+import org.eclipse.jface.text.formatter.IContentFormatter;
+import org.eclipse.jface.text.formatter.IContentFormatterExtension;
+import org.eclipse.jface.text.formatter.IFormattingContext;
+import org.eclipse.jface.text.formatter.IFormattingStrategy;
+import org.eclipse.wst.xml.core.text.IXMLPartitions;
+
+/**
+ * Formatter which replaces the Eclipse formatter for the Android XML editors, and
+ * delegates to it if the user has chosen to use the Eclipse formatter instead by turning
+ * off {@link AdtPrefs#getUseCustomXmlFormatter()}
+ */
+public class AndroidXmlFormatter implements IContentFormatter, IContentFormatterExtension {
+ @Override
+ public final void format(IDocument document, IRegion region) {
+ /**
+ * This method is probably not going to be called. It is part of the
+ * {@link IContentFormatter} but since we also implement
+ * {@link IContentFormatterExtension} Eclipse should /* be calling
+ * {@link #format(IDocument,IFormattingContext)} instead. However, for
+ * completeness (and because other implementations of {@link IContentFormatter}
+ * also do this we might as well make the method behave correctly
+ */
+ FormattingContext context = new FormattingContext();
+ context.setProperty(FormattingContextProperties.CONTEXT_DOCUMENT, Boolean.FALSE);
+ context.setProperty(FormattingContextProperties.CONTEXT_REGION, region);
+
+ format(document, context);
+ }
+
+ @Override
+ public IFormattingStrategy getFormattingStrategy(String contentType) {
+ return new AndroidXmlFormattingStrategy();
+ }
+
+ @Override
+ public void format(IDocument document, IFormattingContext context) {
+ context.setProperty(FormattingContextProperties.CONTEXT_MEDIUM, document);
+ formatMaster(context, document, 0, document.getLength());
+ }
+
+ protected void formatMaster(IFormattingContext context, IDocument document, int offset,
+ int length) {
+ try {
+ final int delta= offset - document.getLineInformationOfOffset(offset).getOffset();
+ offset -= delta;
+ length += delta;
+ } catch (BadLocationException exception) {
+ // Do nothing
+ }
+
+ AndroidXmlFormattingStrategy strategy = new AndroidXmlFormattingStrategy();
+ context.setProperty(FormattingContextProperties.CONTEXT_PARTITION,
+ new TypedPosition(offset, length, IXMLPartitions.XML_DEFAULT));
+ strategy.formatterStarts(context);
+ strategy.format();
+ strategy.formatterStops();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java
new file mode 100644
index 000000000..4cab41962
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java
@@ -0,0 +1,754 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.formatting;
+
+import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
+import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findLineStart;
+import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findTextStart;
+import static com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors.SELECTOR_TAG;
+import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_MEDIUM;
+import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_PARTITION;
+import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_REGION;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
+import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
+
+import com.android.SdkConstants;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatPreferences;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.common.xml.XmlPrettyPrinter;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.jface.text.TypedPosition;
+import org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy;
+import org.eclipse.jface.text.formatter.IFormattingContext;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
+import org.eclipse.wst.xml.ui.internal.XMLFormattingStrategy;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * Formatter which formats XML content according to the established Android coding
+ * conventions. It performs the format by computing the smallest set of DOM nodes
+ * overlapping the formatted region, then it pretty-prints that XML region
+ * using the {@link EclipseXmlPrettyPrinter}, and then it replaces the affected region
+ * by the pretty-printed region.
+ * <p>
+ * This strategy is also used for delegation. If the user has chosen to use the
+ * standard Eclipse XML formatter, this strategy simply delegates to the
+ * default XML formatting strategy in WTP.
+ */
+@SuppressWarnings("restriction")
+public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy {
+ private IRegion mRegion;
+ private final Queue<IDocument> mDocuments = new LinkedList<IDocument>();
+ private final LinkedList<TypedPosition> mPartitions = new LinkedList<TypedPosition>();
+ private ContextBasedFormattingStrategy mDelegate = null;
+ /** False if document is known not to be in an Android project, null until initialized */
+ private Boolean mIsAndroid;
+
+ /**
+ * Creates a new {@link AndroidXmlFormattingStrategy}
+ */
+ public AndroidXmlFormattingStrategy() {
+ }
+
+ private ContextBasedFormattingStrategy getDelegate() {
+ if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()
+ || mIsAndroid != null && !mIsAndroid.booleanValue()) {
+ if (mDelegate == null) {
+ mDelegate = new XMLFormattingStrategy();
+ }
+
+ return mDelegate;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void format() {
+ // Use Eclipse XML formatter instead?
+ ContextBasedFormattingStrategy delegate = getDelegate();
+ if (delegate != null) {
+ delegate.format();
+ return;
+ }
+
+ super.format();
+
+ IDocument document = mDocuments.poll();
+ TypedPosition partition = mPartitions.poll();
+
+ if (document != null && partition != null && mRegion != null) {
+ try {
+ if (document instanceof IStructuredDocument) {
+ IStructuredDocument structuredDocument = (IStructuredDocument) document;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = modelManager.getModelForEdit(structuredDocument);
+ if (model != null) {
+ try {
+ TextEdit edit = format(model, mRegion.getOffset(),
+ mRegion.getLength());
+ if (edit != null) {
+ try {
+ model.aboutToChangeModel();
+ edit.apply(document);
+ }
+ finally {
+ model.changedModel();
+ }
+ }
+ }
+ finally {
+ model.releaseFromEdit();
+ }
+ }
+ }
+ }
+ catch (BadLocationException e) {
+ AdtPlugin.log(e, "Formatting error");
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link TextEdit} for formatting the given model's XML in the text range
+ * starting at offset start with the given length. Note that the exact formatting
+ * offsets may be adjusted to format a complete element.
+ *
+ * @param model the model to be formatted
+ * @param start the starting offset
+ * @param length the length of the text range to be formatted
+ * @return a {@link TextEdit} which edits the model into a formatted document
+ */
+ private static TextEdit format(IStructuredModel model, int start, int length) {
+ int end = start + length;
+
+ TextEdit edit = new MultiTextEdit();
+ IStructuredDocument document = model.getStructuredDocument();
+
+ Node startNode = null;
+ Node endNode = null;
+ Document domDocument = null;
+
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ domDocument = domModel.getDocument();
+ } else {
+ // This should not happen
+ return edit;
+ }
+
+ IStructuredDocumentRegion startRegion = document.getRegionAtCharacterOffset(start);
+ if (startRegion != null) {
+ int startOffset = startRegion.getStartOffset();
+ IndexedRegion currentIndexedRegion = model.getIndexedRegion(startOffset);
+ if (currentIndexedRegion instanceof IDOMNode) {
+ IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
+ startNode = currentDOMNode;
+ }
+ }
+
+ boolean isOpenTagOnly = false;
+ int openTagEnd = -1;
+
+ IStructuredDocumentRegion endRegion = document.getRegionAtCharacterOffset(end);
+ if (endRegion != null) {
+ int endOffset = Math.max(endRegion.getStartOffset(),
+ endRegion.getEndOffset() - 1);
+ IndexedRegion currentIndexedRegion = model.getIndexedRegion(endOffset);
+
+ // If you place the caret right on the right edge of an element, such as this:
+ // <foo name="value">|
+ // then the DOM model will consider the region containing the caret to be
+ // whatever nodes FOLLOWS the element, usually a text node.
+ // Detect this case, and look into the previous range.
+ if (currentIndexedRegion instanceof Text
+ && currentIndexedRegion.getStartOffset() == end && end > 0) {
+ end--;
+ currentIndexedRegion = model.getIndexedRegion(end);
+ endRegion = document.getRegionAtCharacterOffset(
+ currentIndexedRegion.getStartOffset());
+ }
+
+ if (currentIndexedRegion instanceof IDOMNode) {
+ IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
+ endNode = currentDOMNode;
+
+ // See if this range is fully within the opening tag
+ if (endNode == startNode && endRegion == startRegion) {
+ ITextRegion subRegion = endRegion.getRegionAtCharacterOffset(end);
+ ITextRegionList regions = endRegion.getRegions();
+ int index = regions.indexOf(subRegion);
+ if (index != -1) {
+ // Skip past initial occurrence of close tag if we place the caret
+ // right on a >
+ subRegion = regions.get(index);
+ String type = subRegion.getType();
+ if (type == XML_TAG_CLOSE || type == XML_EMPTY_TAG_CLOSE) {
+ index--;
+ }
+ }
+ for (; index >= 0; index--) {
+ subRegion = regions.get(index);
+ String type = subRegion.getType();
+ if (type == XML_TAG_OPEN) {
+ isOpenTagOnly = true;
+ } else if (type == XML_EMPTY_TAG_CLOSE || type == XML_TAG_CLOSE
+ || type == XML_END_TAG_OPEN) {
+ break;
+ }
+ }
+
+ int max = regions.size();
+ for (index = Math.max(0, index); index < max; index++) {
+ subRegion = regions.get(index);
+ String type = subRegion.getType();
+ if (type == XML_EMPTY_TAG_CLOSE || type == XML_TAG_CLOSE) {
+ openTagEnd = subRegion.getEnd() + endRegion.getStartOffset();
+ }
+ }
+
+ if (openTagEnd == -1) {
+ isOpenTagOnly = false;
+ }
+ }
+ }
+ }
+
+ String[] indentationLevels = null;
+ Node root = null;
+ int initialDepth = 0;
+ int replaceStart;
+ int replaceEnd;
+ boolean endWithNewline = false;
+ if (startNode == null || endNode == null) {
+ // Process the entire document
+ root = domDocument;
+ // both document and documentElement should be <= 0
+ initialDepth = -1;
+ startNode = root;
+ endNode = root;
+ replaceStart = 0;
+ replaceEnd = document.getLength();
+ try {
+ endWithNewline = replaceEnd > 0 && document.getChar(replaceEnd - 1) == '\n';
+ } catch (BadLocationException e) {
+ // Can't happen
+ }
+ } else {
+ root = DomUtilities.getCommonAncestor(startNode, endNode);
+ initialDepth = root != null ? DomUtilities.getDepth(root) - 1 : 0;
+
+ // Regions must be non-null since the DOM nodes are non null, but Eclipse null
+ // analysis doesn't realize it:
+ assert startRegion != null && endRegion != null;
+
+ replaceStart = ((IndexedRegion) startNode).getStartOffset();
+ if (isOpenTagOnly) {
+ replaceEnd = openTagEnd;
+ } else {
+ replaceEnd = ((IndexedRegion) endNode).getEndOffset();
+ }
+
+ // Look up the indentation level of the start node, if it is an element
+ // and it starts on its own line
+ if (startNode.getNodeType() == Node.ELEMENT_NODE) {
+ // Measure the indentation of the start node such that we can indent
+ // the reformatted version of the node exactly in place and it should blend
+ // in if the surrounding content does not use the same indentation size etc.
+ // However, it's possible for the start node to have deeper depth than other
+ // content we're formatting, as in the following scenario for example:
+ // <foo>
+ // <bar/>
+ // </foo>
+ // <baz/>
+ // If you select this text range, we want <foo> to be formatted at whatever
+ // level it is, and we also need to know the indentation level to use
+ // for </baz>. We don't measure the depth of <bar/>, a child of the start node,
+ // since from the initial indentation level and on down we want to normalize
+ // the output.
+ IndentationMeasurer m = new IndentationMeasurer(startNode, endNode, document);
+ indentationLevels = m.measure(initialDepth, root);
+
+ // Wipe out any levels deeper than the start node's level
+ // (which may not be the smallest level, e.g. where you select a child
+ // and the end of its parent etc).
+ // (Since we're ONLY measuring the node and its parents, you might wonder
+ // why this is doing a full subtree traversal instead of just walking up
+ // the parent chain and looking up the indentation for each. The reason for
+ // this is that some of theses nodes, which have not yet been formatted,
+ // may be sharing lines with other nodes, and we disregard indentation for
+ // any nodes that don't start a line since the indentation may only be correct
+ // for the first element, so therefore we look for other nodes at the same
+ // level that do have indentation info at the front of the line.
+ int depth = DomUtilities.getDepth(startNode) - 1;
+ for (int i = depth + 1; i < indentationLevels.length; i++) {
+ indentationLevels[i] = null;
+ }
+ }
+ }
+
+ XmlFormatStyle style = guessStyle(model, domDocument);
+ XmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
+ String delimiter = TextUtilities.getDefaultLineDelimiter(document);
+ XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, delimiter);
+ printer.setEndWithNewline(endWithNewline);
+
+ if (indentationLevels != null) {
+ printer.setIndentationLevels(indentationLevels);
+ }
+
+ StringBuilder sb = new StringBuilder(length);
+ printer.prettyPrint(initialDepth, root, startNode, endNode, sb, isOpenTagOnly);
+
+ String formatted = sb.toString();
+ ReplaceEdit replaceEdit = createReplaceEdit(document, replaceStart, replaceEnd, formatted,
+ prefs);
+ if (replaceEdit != null) {
+ edit.addChild(replaceEdit);
+ }
+
+ // Attempt to fix the selection range since otherwise, with the document shifting
+ // under it, you end up selecting a "random" portion of text now shifted into the
+ // old positions of the formatted text:
+ if (replaceEdit != null && replaceStart != 0 && replaceEnd != document.getLength()) {
+ ITextEditor editor = AdtUtils.getActiveTextEditor();
+ if (editor != null) {
+ editor.setHighlightRange(replaceEdit.getOffset(), replaceEdit.getText().length(),
+ false /*moveCursor*/);
+ }
+ }
+
+ return edit;
+ }
+
+ /**
+ * Create a {@link ReplaceEdit} which replaces the text in the given document with the
+ * given new formatted content. The replaceStart and replaceEnd parameters point to
+ * the equivalent unformatted text in the document, but the actual edit range may be
+ * adjusted (for example to make the edit smaller if the beginning and/or end is
+ * identical, and so on)
+ */
+ @VisibleForTesting
+ static ReplaceEdit createReplaceEdit(IDocument document, int replaceStart,
+ int replaceEnd, String formatted, XmlFormatPreferences prefs) {
+ // If replacing a node somewhere in the middle, start the replacement at the
+ // beginning of the current line
+ int index = replaceStart;
+ try {
+ while (index > 0) {
+ char c = document.getChar(index - 1);
+ if (c == '\n') {
+ if (index < replaceStart) {
+ replaceStart = index;
+ }
+ break;
+ } else if (!Character.isWhitespace(c)) {
+ // The replaced node does not start on its own line; in that case,
+ // remove the initial indentation in the reformatted element
+ for (int i = 0; i < formatted.length(); i++) {
+ if (!Character.isWhitespace(formatted.charAt(i))) {
+ formatted = formatted.substring(i);
+ break;
+ }
+ }
+ break;
+ }
+ index--;
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ // If there are multiple blank lines before the insert position, collapse them down
+ // to one
+ int prevNewlineIndex = -1;
+ boolean beginsWithNewline = false;
+ for (int i = 0, n = formatted.length(); i < n; i++) {
+ char c = formatted.charAt(i);
+ if (c == '\n') {
+ beginsWithNewline = true;
+ break;
+ } else if (!Character.isWhitespace(c)) { // \r is whitespace so is handled correctly
+ break;
+ }
+ }
+ try {
+ for (index = replaceStart - 1; index > 0; index--) {
+ char c = document.getChar(index);
+ if (c == '\n') {
+ if (prevNewlineIndex != -1) {
+ replaceStart = prevNewlineIndex;
+ }
+ prevNewlineIndex = index;
+ if (index > 0 && document.getChar(index - 1) == '\r') {
+ prevNewlineIndex--;
+ }
+ } else if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ if (prefs.removeEmptyLines && prevNewlineIndex != -1 && beginsWithNewline) {
+ replaceStart = prevNewlineIndex + 1;
+ }
+
+ // Search forwards too
+ int nextNewlineIndex = -1;
+ try {
+ int max = document.getLength();
+ for (index = replaceEnd; index < max; index++) {
+ char c = document.getChar(index);
+ if (c == '\n') {
+ if (nextNewlineIndex != -1) {
+ replaceEnd = nextNewlineIndex + 1;
+ }
+ nextNewlineIndex = index;
+ } else if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ boolean endsWithNewline = false;
+ for (int i = formatted.length() - 1; i >= 0; i--) {
+ char c = formatted.charAt(i);
+ if (c == '\n') {
+ endsWithNewline = true;
+ break;
+ } else if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+
+ if (prefs.removeEmptyLines && nextNewlineIndex != -1 && endsWithNewline) {
+ replaceEnd = nextNewlineIndex + 1;
+ }
+
+ // Figure out how much of the before and after strings are identical and narrow
+ // the replacement scope
+ boolean foundDifference = false;
+ int firstDifference = 0;
+ int lastDifference = formatted.length();
+ try {
+ for (int i = 0, j = replaceStart; i < formatted.length() && j < replaceEnd; i++, j++) {
+ if (formatted.charAt(i) != document.getChar(j)) {
+ firstDifference = i;
+ foundDifference = true;
+ break;
+ }
+ }
+
+ if (!foundDifference) {
+ // No differences - the document is already formatted, nothing to do
+ return null;
+ }
+
+ lastDifference = firstDifference + 1;
+ for (int i = formatted.length() - 1, j = replaceEnd - 1;
+ i > firstDifference && j > replaceStart;
+ i--, j--) {
+ if (formatted.charAt(i) != document.getChar(j)) {
+ lastDifference = i + 1;
+ break;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ replaceStart += firstDifference;
+ replaceEnd -= (formatted.length() - lastDifference);
+ replaceEnd = Math.max(replaceStart, replaceEnd);
+ formatted = formatted.substring(firstDifference, lastDifference);
+
+ ReplaceEdit replaceEdit = new ReplaceEdit(replaceStart, replaceEnd - replaceStart,
+ formatted);
+ return replaceEdit;
+ }
+
+ /**
+ * Guess what style to use to edit the given document - layout, resource, manifest, ... ? */
+ static XmlFormatStyle guessStyle(IStructuredModel model, Document domDocument) {
+ // The "layout" style is used for most XML resource file types:
+ // layouts, color-lists and state-lists, animations, drawables, menus, etc
+ XmlFormatStyle style = XmlFormatStyle.get(domDocument);
+ if (style == XmlFormatStyle.FILE) {
+ style = XmlFormatStyle.LAYOUT;
+ }
+
+ // The "resource" style is used for most value-based XML files:
+ // strings, dimensions, booleans, colors, integers, plurals,
+ // integer-arrays, string-arrays, and typed-arrays
+ Element rootElement = domDocument.getDocumentElement();
+ if (rootElement != null
+ && SdkConstants.TAG_RESOURCES.equals(rootElement.getTagName())) {
+ style = XmlFormatStyle.RESOURCE;
+ }
+
+ // Selectors are also used similar to resources
+ if (rootElement != null && SELECTOR_TAG.equals(rootElement.getTagName())) {
+ return XmlFormatStyle.RESOURCE;
+ }
+
+ // The "manifest" style is used for manifest files
+ String baseLocation = model.getBaseLocation();
+ if (baseLocation != null) {
+ if (baseLocation.endsWith(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
+ style = XmlFormatStyle.MANIFEST;
+ } else {
+ int lastSlash = baseLocation.lastIndexOf('/');
+ if (lastSlash != -1) {
+ int end = baseLocation.lastIndexOf('/', lastSlash - 1); // -1 is okay
+ String resourceFolder = baseLocation.substring(end + 1, lastSlash);
+ String[] segments = resourceFolder.split("-"); //$NON-NLS-1$
+ ResourceType type = ResourceType.getEnum(segments[0]);
+ if (type != null) {
+ // <resources> files found in res/xml/ should be formatted as
+ // resource files!
+ if (type == ResourceType.XML && style == XmlFormatStyle.RESOURCE) {
+ return style;
+ }
+ style = EclipseXmlPrettyPrinter.get(type);
+ }
+ }
+ }
+ }
+
+ return style;
+ }
+
+ private Boolean isAndroid(IDocument document) {
+ if (mIsAndroid == null) {
+ // Look up the corresponding IResource for this document. This isn't
+ // readily available, so take advantage of the structured model's base location
+ // string and convert it to an IResource to look up its project.
+ if (document instanceof IStructuredDocument) {
+ IStructuredDocument structuredDocument = (IStructuredDocument) document;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+
+ IStructuredModel model = modelManager.getModelForRead(structuredDocument);
+ if (model != null) {
+ String location = model.getBaseLocation();
+ model.releaseFromRead();
+ if (location != null) {
+ if (!location.endsWith(ANDROID_MANIFEST_XML)
+ && !location.contains("/res/")) { //$NON-NLS-1$
+ // See if it looks like a foreign document
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IWorkspaceRoot root = workspace.getRoot();
+ IResource member = root.findMember(location);
+ if (member.exists()) {
+ IProject project = member.getProject();
+ if (project.isAccessible() &&
+ !BaseProjectHelper.isAndroidProject(project)) {
+ mIsAndroid = false;
+ return false;
+ }
+ }
+ }
+ // Ignore Maven POM files even in Android projects
+ if (location.endsWith("/pom.xml")) { //$NON-NLS-1$
+ mIsAndroid = false;
+ return false;
+ }
+ }
+ }
+ }
+
+ mIsAndroid = true;
+ }
+
+ return mIsAndroid.booleanValue();
+ }
+
+ @Override
+ public void formatterStarts(final IFormattingContext context) {
+ // Use Eclipse XML formatter instead?
+ ContextBasedFormattingStrategy delegate = getDelegate();
+ if (delegate != null) {
+ delegate.formatterStarts(context);
+
+ // We also need the super implementation because it stores items into the
+ // map, and we can't override the getPreferences method, so we need for
+ // this delegating strategy to supply the correct values when it is called
+ // instead of the delegate
+ super.formatterStarts(context);
+
+ return;
+ }
+
+ super.formatterStarts(context);
+ mRegion = (IRegion) context.getProperty(CONTEXT_REGION);
+ TypedPosition partition = (TypedPosition) context.getProperty(CONTEXT_PARTITION);
+ IDocument document = (IDocument) context.getProperty(CONTEXT_MEDIUM);
+ mPartitions.offer(partition);
+ mDocuments.offer(document);
+
+ if (!isAndroid(document)) {
+ // It's some foreign type of project: use default
+ // formatter
+ delegate = getDelegate();
+ if (delegate != null) {
+ delegate.formatterStarts(context);
+ }
+ }
+ }
+
+ @Override
+ public void formatterStops() {
+ // Use Eclipse XML formatter instead?
+ ContextBasedFormattingStrategy delegate = getDelegate();
+ if (delegate != null) {
+ delegate.formatterStops();
+ // See formatterStarts for an explanation
+ super.formatterStops();
+
+ return;
+ }
+
+ super.formatterStops();
+ mRegion = null;
+ mDocuments.clear();
+ mPartitions.clear();
+ }
+
+ /**
+ * Utility class which can measure the indentation strings for various node levels in
+ * a given node range
+ */
+ static class IndentationMeasurer {
+ private final Map<Integer, String> mDepth = new HashMap<Integer, String>();
+ private final Node mStartNode;
+ private final Node mEndNode;
+ private final IStructuredDocument mDocument;
+ private boolean mDone = false;
+ private boolean mInRange = false;
+ private int mMaxDepth;
+
+ public IndentationMeasurer(Node mStartNode, Node mEndNode, IStructuredDocument document) {
+ super();
+ this.mStartNode = mStartNode;
+ this.mEndNode = mEndNode;
+ mDocument = document;
+ }
+
+ /**
+ * Measure the various depths found in the range (defined in the constructor)
+ * under the given node which should be a common ancestor of the start and end
+ * nodes. The result is a string array where each index corresponds to a depth,
+ * and the string is either empty, or the complete indentation string to be used
+ * to indent to the given depth (note that these strings are not cumulative)
+ *
+ * @param initialDepth the initial depth to use when visiting
+ * @param root the root node to look for depths under
+ * @return a string array containing nulls or indentation strings
+ */
+ public String[] measure(int initialDepth, Node root) {
+ visit(initialDepth, root);
+ String[] indentationLevels = new String[mMaxDepth + 1];
+ for (Map.Entry<Integer, String> entry : mDepth.entrySet()) {
+ int depth = entry.getKey();
+ String indentation = entry.getValue();
+ indentationLevels[depth] = indentation;
+ }
+
+ return indentationLevels;
+ }
+
+ private void visit(int depth, Node node) {
+ // Look up indentation for this level
+ if (node.getNodeType() == Node.ELEMENT_NODE && mDepth.get(depth) == null) {
+ // Look up the depth
+ try {
+ IndexedRegion region = (IndexedRegion) node;
+ int lineStart = findLineStart(mDocument, region.getStartOffset());
+ int textStart = findTextStart(mDocument, lineStart, region.getEndOffset());
+
+ // Ensure that the text which begins the line is this element, otherwise
+ // we could be measuring the indentation of a parent element which begins
+ // the line
+ if (textStart == region.getStartOffset()) {
+ String indent = mDocument.get(lineStart,
+ Math.max(0, textStart - lineStart));
+ mDepth.put(depth, indent);
+
+ if (depth > mMaxDepth) {
+ mMaxDepth = depth;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ visit(depth + 1, child);
+ if (mDone) {
+ return;
+ }
+ }
+
+ if (node == mEndNode) {
+ mDone = true;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlFormatPreferences.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlFormatPreferences.java
new file mode 100644
index 000000000..6c00b8ee2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlFormatPreferences.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.formatting;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.common.xml.XmlAttributeSortOrder;
+
+import org.eclipse.core.runtime.Preferences;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.ui.internal.editors.text.EditorsPlugin;
+import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.xml.core.internal.XMLCorePlugin;
+import org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames;
+import org.w3c.dom.Attr;
+
+import java.util.Comparator;
+
+/**
+ * Formatting preferences used by the Android XML formatter.
+ */
+public class EclipseXmlFormatPreferences extends XmlFormatPreferences {
+ @VisibleForTesting
+ protected EclipseXmlFormatPreferences() {
+ }
+
+ /**
+ * Creates a new {@link EclipseXmlFormatPreferences} based on the current settings
+ * in {@link AdtPrefs}
+ *
+ * @return an {@link EclipseXmlFormatPreferences} object
+ */
+ @NonNull
+ public static EclipseXmlFormatPreferences create() {
+ EclipseXmlFormatPreferences p = new EclipseXmlFormatPreferences();
+ AdtPrefs prefs = AdtPrefs.getPrefs();
+
+ p.useEclipseIndent = prefs.isUseEclipseIndent();
+ p.removeEmptyLines = prefs.isRemoveEmptyLines();
+ p.oneAttributeOnFirstLine = prefs.isOneAttributeOnFirstLine();
+ p.sortAttributes = prefs.getAttributeSort();
+ p.spaceBeforeClose = prefs.isSpaceBeforeClose();
+
+ return p;
+ }
+
+ @Override
+ @Nullable
+ public Comparator<Attr> getAttributeComparator() {
+ // Can't just skip sorting; the attribute table moves attributes out of order
+ // due to hashing, so sort by node positions
+ if (sortAttributes == XmlAttributeSortOrder.NO_SORTING) {
+ return EXISTING_ORDER_COMPARATOR;
+ }
+ return sortAttributes.getAttributeComparator();
+ }
+
+ private static final Comparator<Attr> EXISTING_ORDER_COMPARATOR = new Comparator<Attr>() {
+ @Override
+ public int compare(Attr attr1, Attr attr2) {
+ IndexedRegion region1 = (IndexedRegion) attr1;
+ IndexedRegion region2 = (IndexedRegion) attr2;
+
+ return region1.getStartOffset() - region2.getStartOffset();
+ }
+ };
+
+ // The XML module settings do not have a public API. We should replace this with JDT
+ // settings anyway since that's more likely what users have configured and want applied
+ // to their XML files
+
+ /**
+ * Returns the string to use to indent one indentation level
+ *
+ * @return the string used to indent one indentation level
+ */
+ @Override
+ @SuppressWarnings({
+ "restriction", "deprecation"
+ })
+ public String getOneIndentUnit() {
+ if (useEclipseIndent) {
+ // Look up Eclipse indent preferences
+ // TODO: Use the JDT preferences instead, which make more sense
+ Preferences preferences = XMLCorePlugin.getDefault().getPluginPreferences();
+ int indentationWidth = preferences.getInt(XMLCorePreferenceNames.INDENTATION_SIZE);
+ String indentCharPref = preferences.getString(XMLCorePreferenceNames.INDENTATION_CHAR);
+ boolean useSpaces = XMLCorePreferenceNames.SPACE.equals(indentCharPref);
+
+ StringBuilder indentString = new StringBuilder();
+ for (int j = 0; j < indentationWidth; j++) {
+ if (useSpaces) {
+ indentString.append(' ');
+ } else {
+ indentString.append('\t');
+ }
+ }
+ mOneIndentUnit = indentString.toString();
+ }
+
+ return mOneIndentUnit;
+ }
+
+ /**
+ * Returns the number of spaces used to display a single tab character
+ *
+ * @return the number of spaces used to display a single tab character
+ */
+ @Override
+ @SuppressWarnings("restriction") // Editor settings
+ public int getTabWidth() {
+ if (mTabWidth == -1) {
+ String key = AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH;
+ try {
+ IPreferenceStore prefs = EditorsPlugin.getDefault().getPreferenceStore();
+ mTabWidth = prefs.getInt(key);
+ } catch (Throwable t) {
+ // Pass: We'll pick a suitable default instead below
+ }
+ if (mTabWidth <= 0) {
+ mTabWidth = 4;
+ }
+ }
+
+ return mTabWidth;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java
new file mode 100644
index 000000000..d3f7ec866
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.formatting;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.xml.XmlFormatPreferences;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.common.xml.XmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.utils.SdkUtils;
+import com.android.utils.XmlUtils;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Eclipse customization of the {@link EclipseXmlPrettyPrinter} which takes advantage of the
+ * Eclipse DOM Api to track additional information, such as whether an element with no children
+ * was of the open form ({@code <foo></foo>}) or the closed form ({@code <foo/>}), the ability to
+ * look up the original source (for proper entity handling), the ability to preserve attribute
+ * source order, etc.
+ */
+@SuppressWarnings("restriction") // WST XML API
+public class EclipseXmlPrettyPrinter extends XmlPrettyPrinter {
+
+ /**
+ * Creates a new {@link com.android.ide.common.xml.XmlPrettyPrinter}
+ *
+ * @param prefs the preferences to format with
+ * @param style the style to format with
+ * @param lineSeparator the line separator to use, such as "\n" (can be null, in which case the
+ * system default is looked up via the line.separator property)
+ */
+ public EclipseXmlPrettyPrinter(
+ XmlFormatPreferences prefs,
+ XmlFormatStyle style,
+ String lineSeparator) {
+ super(prefs, style, lineSeparator == null ? getDefaultLineSeparator() : lineSeparator);
+ }
+
+ /**
+ * Pretty-prints the given XML document, which must be well-formed. If it is not,
+ * the original unformatted XML document is returned
+ *
+ * @param xml the XML content to format
+ * @param prefs the preferences to format with
+ * @param style the style to format with
+ * @param lineSeparator the line separator to use, such as "\n" (can be null, in which
+ * case the system default is looked up via the line.separator property)
+ * @return the formatted document (or if a parsing error occurred, returns the
+ * unformatted document)
+ */
+ @NonNull
+ public static String prettyPrint(
+ @NonNull String xml,
+ @NonNull XmlFormatPreferences prefs,
+ @NonNull XmlFormatStyle style,
+ @Nullable String lineSeparator) {
+ Document document = DomUtilities.parseStructuredDocument(xml);
+ if (document != null) {
+ EclipseXmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style,
+ lineSeparator);
+ if (xml.endsWith("\n")) { //$NON-NLS-1$
+ printer.setEndWithNewline(true);
+ }
+
+ StringBuilder sb = new StringBuilder(3 * xml.length() / 2);
+ printer.prettyPrint(-1, document, null, null, sb, false /*openTagOnly*/);
+ return sb.toString();
+ } else {
+ // Parser error: just return the unformatted content
+ return xml;
+ }
+ }
+
+ @NonNull
+ public static String prettyPrint(@NonNull Node node, boolean endWithNewline) {
+ return prettyPrint(node, EclipseXmlFormatPreferences.create(), XmlFormatStyle.get(node),
+ null, endWithNewline);
+ }
+
+ private static String getDefaultLineSeparator() {
+ org.eclipse.jface.text.Document blank = new org.eclipse.jface.text.Document();
+ String lineSeparator = TextUtilities.getDefaultLineDelimiter(blank);
+ if (lineSeparator == null) {
+ lineSeparator = SdkUtils.getLineSeparator();
+ }
+
+ return lineSeparator;
+ }
+
+ /**
+ * Pretty prints the given node
+ *
+ * @param node the node, usually a document, to be printed
+ * @param prefs the formatting preferences
+ * @param style the formatting style to use
+ * @param lineSeparator the line separator to use, or null to use the
+ * default
+ * @return a formatted string
+ */
+ @NonNull
+ public static String prettyPrint(
+ @NonNull Node node,
+ @NonNull XmlFormatPreferences prefs,
+ @NonNull XmlFormatStyle style,
+ @Nullable String lineSeparator,
+ boolean endWithNewline) {
+ XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, lineSeparator);
+ printer.setEndWithNewline(endWithNewline);
+ StringBuilder sb = new StringBuilder(1000);
+ printer.prettyPrint(-1, node, null, null, sb, false /*openTagOnly*/);
+ String xml = sb.toString();
+ if (node.getNodeType() == Node.DOCUMENT_NODE && !xml.startsWith("<?")) { //$NON-NLS-1$
+ xml = XmlUtils.XML_PROLOG + xml;
+ }
+ return xml;
+ }
+
+ @Nullable
+ @Override
+ protected String getSource(@NonNull Node node) {
+ // In Eclipse, org.w3c.dom.DocumentType.getTextContent() returns null
+ if (node instanceof IDOMNode) {
+ // Get the original source string. This will contain the actual entities
+ // such as "&gt;" instead of ">" which it gets turned into for the DOM nodes.
+ // By operating on source we can preserve the user's entities rather than
+ // having &gt; for example always turned into >.
+ IDOMNode textImpl = (IDOMNode) node;
+ return textImpl.getSource();
+ }
+
+ return super.getSource(node);
+ }
+
+ @Override
+ protected boolean isEmptyTag(Element element) {
+ if (element instanceof IDOMElement) {
+ IDOMElement elementImpl = (IDOMElement) element;
+ if (elementImpl.isEmptyTag()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the {@link XmlFormatStyle} to use for a resource of the given type
+ *
+ * @param resourceType the type of resource to be formatted
+ * @return the suitable format style to use
+ */
+ public static XmlFormatStyle get(ResourceType resourceType) {
+ switch (resourceType) {
+ case ARRAY:
+ case ATTR:
+ case BOOL:
+ case DECLARE_STYLEABLE:
+ case DIMEN:
+ case FRACTION:
+ case ID:
+ case INTEGER:
+ case STRING:
+ case PLURALS:
+ case STYLE:
+ case STYLEABLE:
+ case COLOR:
+ return XmlFormatStyle.RESOURCE;
+
+ case LAYOUT:
+ return XmlFormatStyle.LAYOUT;
+
+ case DRAWABLE:
+ case MENU:
+ case ANIM:
+ case ANIMATOR:
+ case INTERPOLATOR:
+ default:
+ return XmlFormatStyle.FILE;
+ }
+ }
+
+ /**
+ * Returns the {@link XmlFormatStyle} to use for resource files in the given resource
+ * folder
+ *
+ * @param folderType the type of folder containing the resource file
+ * @return the suitable format style to use
+ */
+ public static XmlFormatStyle getForFolderType(ResourceFolderType folderType) {
+ switch (folderType) {
+ case LAYOUT:
+ return XmlFormatStyle.LAYOUT;
+ case COLOR:
+ case VALUES:
+ return XmlFormatStyle.RESOURCE;
+ case ANIM:
+ case ANIMATOR:
+ case DRAWABLE:
+ case INTERPOLATOR:
+ case MENU:
+ default:
+ return XmlFormatStyle.FILE;
+ }
+ }
+
+ /**
+ * Returns the {@link XmlFormatStyle} to use for resource files of the given path.
+ *
+ * @param path the path to the resource file
+ * @return the suitable format style to use
+ */
+ public static XmlFormatStyle getForFile(IPath path) {
+ if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(path.lastSegment())) {
+ return XmlFormatStyle.MANIFEST;
+ }
+
+ if (path.segmentCount() > 2) {
+ String parentName = path.segment(path.segmentCount() - 2);
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName);
+ return getForFolderType(folderType);
+ }
+
+ return XmlFormatStyle.FILE;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatProcessor.java
new file mode 100644
index 000000000..3f833029d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatProcessor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.formatting;
+
+import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_MEDIUM;
+import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_PARTITION;
+import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_REGION;
+
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.jface.text.TypedPosition;
+import org.eclipse.jface.text.formatter.FormattingContext;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.formatter.XMLFormatterFormatProcessor;
+import org.eclipse.wst.xml.core.text.IXMLPartitions;
+
+/**
+ * Customized version of the builtin XML format processor which delegates to the
+ * Android specific formatter such that applying format on IFiles work as
+ * expected
+ */
+@SuppressWarnings("restriction")
+public class XmlFormatProcessor extends XMLFormatterFormatProcessor {
+ /** Constructs a new {@link XmlFormatProcessor} */
+ public XmlFormatProcessor() {
+ }
+
+ @Override
+ public void formatModel(IStructuredModel structuredModel, int start, int length) {
+ if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) {
+ super.formatModel(structuredModel, start, length);
+ return;
+ }
+
+ AndroidXmlFormatter formatter = new AndroidXmlFormatter();
+ IStructuredDocument document = structuredModel.getStructuredDocument();
+ FormattingContext context = new FormattingContext();
+ context.setProperty(CONTEXT_MEDIUM, document);
+ context.setProperty(CONTEXT_PARTITION, new TypedPosition(start, length,
+ IXMLPartitions.XML_DEFAULT));
+ context.setProperty(CONTEXT_REGION, new org.eclipse.jface.text.Region(start, length));
+ formatter.formatMaster(context, document, start, length);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlQuickAssistManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlQuickAssistManager.java
new file mode 100644
index 000000000..a979a8086
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlQuickAssistManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.formatting;
+
+import com.android.ide.eclipse.adt.internal.build.AaptQuickFix;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.RefactoringAssistant;
+import com.android.ide.eclipse.adt.internal.lint.LintFixGenerator;
+
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+import org.eclipse.jface.text.source.Annotation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class implements Quick Assists for XML files. It does not perform any
+ * quick assistance on its own, but it coordinates the various separate quick
+ * assists available for XML such that the order is logical. This is necessary
+ * because without it, the order of suggestions (when more than one assistant
+ * provides suggestions) is not always optimal. There doesn't seem to be a way
+ * from non-Java languages to set the sorting order (see
+ * https://bugs.eclipse.org/bugs/show_bug.cgi?id=229983 ). So instead of
+ * registering our multiple XML quick assistants via the plugin.xml file, we
+ * register <b>just</b> this manager, which delegates to the various XML quick
+ * assistants as appropriate.
+ */
+public class XmlQuickAssistManager implements IQuickAssistProcessor {
+ private final IQuickAssistProcessor[] mProcessors;
+
+ /** Constructs a new {@link XmlQuickAssistManager} which orders the quick fixes */
+ public XmlQuickAssistManager() {
+ mProcessors = new IQuickAssistProcessor[] {
+ new AaptQuickFix(),
+ new LintFixGenerator(),
+ new RefactoringAssistant()
+ };
+ }
+
+ @Override
+ public String getErrorMessage() {
+ return null;
+ }
+
+ @Override
+ public boolean canFix(Annotation annotation) {
+ for (IQuickAssistProcessor processor : mProcessors) {
+ if (processor.canFix(annotation)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean canAssist(IQuickAssistInvocationContext invocationContext) {
+ for (IQuickAssistProcessor processor : mProcessors) {
+ if (processor.canAssist(invocationContext)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public ICompletionProposal[] computeQuickAssistProposals(
+ IQuickAssistInvocationContext invocationContext) {
+ List<ICompletionProposal> allProposals = null;
+ for (IQuickAssistProcessor processor : mProcessors) {
+ if (processor.canAssist(invocationContext)) {
+ ICompletionProposal[] proposals =
+ processor.computeQuickAssistProposals(invocationContext);
+ if (proposals != null && proposals.length > 0) {
+ if (allProposals == null) {
+ allProposals = new ArrayList<ICompletionProposal>();
+ }
+ for (ICompletionProposal proposal : proposals) {
+ allProposals.add(proposal);
+ }
+ }
+ }
+ }
+
+ if (allProposals != null) {
+ return allProposals.toArray(new ICompletionProposal[allProposals.size()]);
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ActionBarHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ActionBarHandler.java
new file mode 100644
index 000000000..49585a3ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ActionBarHandler.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+import com.android.ide.common.rendering.api.ActionBarCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VALUE_SPLIT_ACTION_BAR_WHEN_NARROW;
+
+public class ActionBarHandler extends ActionBarCallback {
+
+ private final GraphicalEditorPart mEditor;
+
+ ActionBarHandler(GraphicalEditorPart editor) {
+ mEditor = editor;
+ }
+
+ @Override
+ public List<String> getMenuIdNames() {
+ String commaSeparatedMenus = getXmlAttribute(ATTR_MENU);
+ List<String> menus = new ArrayList<String>();
+ Iterables.addAll(menus, Splitter.on(',').trimResults().omitEmptyStrings()
+ .split(commaSeparatedMenus));
+ return menus;
+ }
+
+ @Override
+ public boolean getSplitActionBarWhenNarrow() {
+ ActivityAttributes attributes = getActivityAttributes();
+ if (attributes != null) {
+ return VALUE_SPLIT_ACTION_BAR_WHEN_NARROW.equals(attributes.getUiOptions());
+ }
+ return false;
+ }
+
+ @Override
+ public int getNavigationMode() {
+ String navMode = getXmlAttribute(ATTR_NAV_MODE);
+ if (navMode.equalsIgnoreCase(VALUE_NAV_MODE_TABS)) {
+ return NAVIGATION_MODE_TABS;
+ }
+ if (navMode.equalsIgnoreCase(VALUE_NAV_MODE_LIST)) {
+ return NAVIGATION_MODE_LIST;
+ }
+ return NAVIGATION_MODE_STANDARD;
+ }
+
+ @Override
+ public HomeButtonStyle getHomeButtonStyle() {
+ ActivityAttributes attributes = getActivityAttributes();
+ if (attributes != null && attributes.getParentActivity() != null) {
+ return HomeButtonStyle.SHOW_HOME_AS_UP;
+ }
+ return HomeButtonStyle.NONE;
+ }
+
+ private ActivityAttributes getActivityAttributes() {
+ ManifestInfo manifest = ManifestInfo.get(mEditor.getProject());
+ String activity = mEditor.getConfigurationChooser().getConfiguration().getActivity();
+ return manifest.getActivityAttributes(activity);
+ }
+
+ private String getXmlAttribute(String name) {
+ Element element = mEditor.getModel().getUiRoot().getXmlDocument().getDocumentElement();
+ String value = element.getAttributeNS(TOOLS_URI, name);
+ if (value == null) {
+ return "";
+ }
+ return value.trim();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/BasePullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/BasePullParser.java
new file mode 100644
index 000000000..43fb1a5bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/BasePullParser.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import com.android.ide.common.rendering.legacy.ILegacyPullParser;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ * Base implementation of an {@link ILegacyPullParser} for cases where the parser is not sitting
+ * on top of an actual XML file.
+ * <p/>It's designed to work on layout files, and will most likely not work on other resource
+ * files.
+ */
+public abstract class BasePullParser implements ILegacyPullParser {
+
+ protected int mParsingState = START_DOCUMENT;
+
+ public BasePullParser() {
+ }
+
+ // --- new methods to override ---
+
+ public abstract void onNextFromStartDocument();
+ public abstract void onNextFromStartTag();
+ public abstract void onNextFromEndTag();
+
+ // --- basic implementation of IXmlPullParser ---
+
+ @Override
+ public void setFeature(String name, boolean state) throws XmlPullParserException {
+ if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) {
+ return;
+ }
+ if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) {
+ return;
+ }
+ throw new XmlPullParserException("Unsupported feature: " + name);
+ }
+
+ @Override
+ public boolean getFeature(String name) {
+ if (FEATURE_PROCESS_NAMESPACES.equals(name)) {
+ return true;
+ }
+ if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void setProperty(String name, Object value) throws XmlPullParserException {
+ throw new XmlPullParserException("setProperty() not supported");
+ }
+
+ @Override
+ public Object getProperty(String name) {
+ return null;
+ }
+
+ @Override
+ public void setInput(Reader in) throws XmlPullParserException {
+ throw new XmlPullParserException("setInput() not supported");
+ }
+
+ @Override
+ public void setInput(InputStream inputStream, String inputEncoding)
+ throws XmlPullParserException {
+ throw new XmlPullParserException("setInput() not supported");
+ }
+
+ @Override
+ public void defineEntityReplacementText(String entityName, String replacementText)
+ throws XmlPullParserException {
+ throw new XmlPullParserException("defineEntityReplacementText() not supported");
+ }
+
+ @Override
+ public String getNamespacePrefix(int pos) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespacePrefix() not supported");
+ }
+
+ @Override
+ public String getInputEncoding() {
+ return null;
+ }
+
+ @Override
+ public String getNamespace(String prefix) {
+ throw new RuntimeException("getNamespace() not supported");
+ }
+
+ @Override
+ public int getNamespaceCount(int depth) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespaceCount() not supported");
+ }
+
+ @Override
+ public String getNamespaceUri(int pos) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespaceUri() not supported");
+ }
+
+ @Override
+ public int getColumnNumber() {
+ return -1;
+ }
+
+ @Override
+ public int getLineNumber() {
+ return -1;
+ }
+
+ @Override
+ public String getAttributeType(int arg0) {
+ return "CDATA";
+ }
+
+ @Override
+ public int getEventType() {
+ return mParsingState;
+ }
+
+ @Override
+ public String getText() {
+ return null;
+ }
+
+ @Override
+ public char[] getTextCharacters(int[] arg0) {
+ return null;
+ }
+
+ @Override
+ public boolean isAttributeDefault(int arg0) {
+ return false;
+ }
+
+ @Override
+ public boolean isWhitespace() {
+ return false;
+ }
+
+ @Override
+ public int next() throws XmlPullParserException {
+ switch (mParsingState) {
+ case END_DOCUMENT:
+ throw new XmlPullParserException("Nothing after the end");
+ case START_DOCUMENT:
+ onNextFromStartDocument();
+ break;
+ case START_TAG:
+ onNextFromStartTag();
+ break;
+ case END_TAG:
+ onNextFromEndTag();
+ break;
+ case TEXT:
+ // not used
+ break;
+ case CDSECT:
+ // not used
+ break;
+ case ENTITY_REF:
+ // not used
+ break;
+ case IGNORABLE_WHITESPACE:
+ // not used
+ break;
+ case PROCESSING_INSTRUCTION:
+ // not used
+ break;
+ case COMMENT:
+ // not used
+ break;
+ case DOCDECL:
+ // not used
+ break;
+ }
+
+ return mParsingState;
+ }
+
+ @Override
+ public int nextTag() throws XmlPullParserException {
+ int eventType = next();
+ if (eventType != START_TAG && eventType != END_TAG) {
+ throw new XmlPullParserException("expected start or end tag", this, null);
+ }
+ return eventType;
+ }
+
+ @Override
+ public String nextText() throws XmlPullParserException {
+ if (getEventType() != START_TAG) {
+ throw new XmlPullParserException("parser must be on START_TAG to read next text", this,
+ null);
+ }
+ int eventType = next();
+ if (eventType == TEXT) {
+ String result = getText();
+ eventType = next();
+ if (eventType != END_TAG) {
+ throw new XmlPullParserException(
+ "event TEXT it must be immediately followed by END_TAG", this, null);
+ }
+ return result;
+ } else if (eventType == END_TAG) {
+ return "";
+ } else {
+ throw new XmlPullParserException("parser must be on START_TAG or TEXT to read text",
+ this, null);
+ }
+ }
+
+ @Override
+ public int nextToken() throws XmlPullParserException {
+ return next();
+ }
+
+ @Override
+ public void require(int type, String namespace, String name) throws XmlPullParserException {
+ if (type != getEventType() || (namespace != null && !namespace.equals(getNamespace()))
+ || (name != null && !name.equals(getName())))
+ throw new XmlPullParserException("expected " + TYPES[type] + getPositionDescription());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java
new file mode 100644
index 000000000..f30406520
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import static com.android.SdkConstants.ATTR_IGNORE;
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.LIST_VIEW;
+import static com.android.SdkConstants.SPINNER;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_FRAGMENT_LAYOUT;
+
+import com.android.SdkConstants;
+import com.android.ide.common.rendering.api.ILayoutPullParser;
+import com.android.ide.common.rendering.api.IProjectCallback;
+import com.android.ide.common.res2.ValueXmlHelper;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata;
+import com.google.common.collect.Maps;
+
+import org.kxml2.io.KXmlParser;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Modified {@link KXmlParser} that adds the methods of {@link ILayoutPullParser}, and
+ * performs other layout-specific parser behavior like translating fragment tags into
+ * include tags.
+ * <p/>
+ * It will return a given parser when queried for one through
+ * {@link ILayoutPullParser#getParser(String)} for a given name.
+ *
+ */
+public class ContextPullParser extends KXmlParser implements ILayoutPullParser {
+ private static final String COMMENT_PREFIX = "<!--"; //$NON-NLS-1$
+ private static final String COMMENT_SUFFIX = "-->"; //$NON-NLS-1$
+ /** The callback to request parsers from */
+ private final IProjectCallback mProjectCallback;
+ /** The {@link File} for the layout currently being parsed */
+ private File mFile;
+ /** The layout to be shown for the current {@code <fragment>} tag. Usually null. */
+ private String mFragmentLayout = null;
+
+ /**
+ * Creates a new {@link ContextPullParser}
+ *
+ * @param projectCallback the associated callback
+ * @param file the file to be parsed
+ */
+ public ContextPullParser(IProjectCallback projectCallback, File file) {
+ super();
+ mProjectCallback = projectCallback;
+ mFile = file;
+ }
+
+ // --- Layout lib API methods
+
+ @Override
+ /**
+ * this is deprecated but must still be implemented for older layout libraries.
+ * @deprecated use {@link IProjectCallback#getParser(String)}.
+ */
+ @Deprecated
+ public ILayoutPullParser getParser(String layoutName) {
+ return mProjectCallback.getParser(layoutName);
+ }
+
+ @Override
+ public Object getViewCookie() {
+ String name = super.getName();
+ if (name == null) {
+ return null;
+ }
+
+ // Store tools attributes if this looks like a layout we'll need adapter view
+ // bindings for in the ProjectCallback.
+ if (LIST_VIEW.equals(name)
+ || EXPANDABLE_LIST_VIEW.equals(name)
+ || GRID_VIEW.equals(name)
+ || SPINNER.equals(name)) {
+ Map<String, String> map = null;
+ int count = getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ String namespace = getAttributeNamespace(i);
+ if (namespace != null && namespace.equals(TOOLS_URI)) {
+ String attribute = getAttributeName(i);
+ if (attribute.equals(ATTR_IGNORE)) {
+ continue;
+ }
+ if (map == null) {
+ map = Maps.newHashMapWithExpectedSize(4);
+ }
+ map.put(attribute, getAttributeValue(i));
+ }
+ }
+
+ return map;
+ }
+
+ return null;
+ }
+
+ // --- KXMLParser override
+
+ @Override
+ public String getName() {
+ String name = super.getName();
+
+ // At designtime, replace fragments with includes.
+ if (name.equals(VIEW_FRAGMENT)) {
+ mFragmentLayout = LayoutMetadata.getProperty(this, KEY_FRAGMENT_LAYOUT);
+ if (mFragmentLayout != null) {
+ return VIEW_INCLUDE;
+ }
+ } else {
+ mFragmentLayout = null;
+ }
+
+
+ return name;
+ }
+
+ @Override
+ public String getAttributeValue(String namespace, String localName) {
+ if (ATTR_LAYOUT.equals(localName) && mFragmentLayout != null) {
+ return mFragmentLayout;
+ }
+
+ String value = super.getAttributeValue(namespace, localName);
+
+ // on the fly convert match_parent to fill_parent for compatibility with older
+ // platforms.
+ if (VALUE_MATCH_PARENT.equals(value) &&
+ (ATTR_LAYOUT_WIDTH.equals(localName) ||
+ ATTR_LAYOUT_HEIGHT.equals(localName)) &&
+ SdkConstants.NS_RESOURCES.equals(namespace)) {
+ return VALUE_FILL_PARENT;
+ }
+
+ // Handle unicode escapes etc
+ value = ValueXmlHelper.unescapeResourceString(value, false, false);
+
+ return value;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ExplodedRenderingHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ExplodedRenderingHelper.java
new file mode 100644
index 000000000..25fa3e991
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ExplodedRenderingHelper.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * This class computes the new screen size in "exploded rendering" mode.
+ * It goes through the whole layout tree and figures out how many embedded layouts will have
+ * extra padding and compute how that will affect the screen size.
+ *
+ * TODO
+ * - find a better class name :)
+ * - move the logic for each layout to the layout rule classes?
+ * - support custom classes (by querying JDT for its super class and reverting to its behavior)
+ */
+public final class ExplodedRenderingHelper {
+ /** value of the padding in pixel.
+ * TODO: make a preference?
+ */
+ public final static int PADDING_VALUE = 10;
+
+ private final int[] mPadding = new int[] { 0, 0 };
+ private Set<String> mLayoutNames;
+
+ /**
+ * Computes the padding. access the result through {@link #getWidthPadding()} and
+ * {@link #getHeightPadding()}.
+ * @param root the root node (ie the top layout).
+ * @param iProject the project to which the layout belong.
+ */
+ public ExplodedRenderingHelper(Node root, IProject iProject) {
+ // get the layout descriptors to get the name of all the layout classes.
+ IAndroidTarget target = Sdk.getCurrent().getTarget(iProject);
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ LayoutDescriptors descriptors = data.getLayoutDescriptors();
+
+ mLayoutNames = new HashSet<String>();
+ List<ViewElementDescriptor> layoutDescriptors = descriptors.getLayoutDescriptors();
+ for (ViewElementDescriptor desc : layoutDescriptors) {
+ mLayoutNames.add(desc.getXmlLocalName());
+ }
+
+ computePadding(root, mPadding);
+ }
+
+ /**
+ * (Unit tests only)
+ * Computes the padding. access the result through {@link #getWidthPadding()} and
+ * {@link #getHeightPadding()}.
+ * @param root the root node (ie the top layout).
+ * @param layoutNames the list of layout classes
+ */
+ public ExplodedRenderingHelper(Node root, Set<String> layoutNames) {
+ mLayoutNames = layoutNames;
+
+ computePadding(root, mPadding);
+ }
+
+ /**
+ * Returns the number of extra padding in the X axis. This doesn't return a number of pixel
+ * or dip, but how many paddings are pushing the screen dimension out.
+ */
+ public int getWidthPadding() {
+ return mPadding[0];
+ }
+
+ /**
+ * Returns the number of extra padding in the Y axis. This doesn't return a number of pixel
+ * or dip, but how many paddings are pushing the screen dimension out.
+ */
+ public int getHeightPadding() {
+ return mPadding[1];
+ }
+
+ /**
+ * Computes the number of padding for a given view, and fills the given array of int.
+ * <p/>index 0 is X axis, index 1 is Y axis
+ * @param view the view to compute
+ * @param padding the result padding (index 0 is X axis, index 1 is Y axis)
+ */
+ private void computePadding(Node view, int[] padding) {
+ String localName = view.getLocalName();
+
+ // first compute for each children
+ NodeList children = view.getChildNodes();
+ int count = children.getLength();
+ if (count > 0) {
+ // compute the padding for all the children.
+ Map<Node, int[]> childrenPadding = new HashMap<Node, int[]>(count);
+ for (int i = 0 ; i < count ; i++) {
+ Node child = children.item(i);
+ short type = child.getNodeType();
+ if (type == Node.ELEMENT_NODE) { // ignore TEXT/CDATA nodes.
+ int[] p = new int[] { 0, 0 };
+ childrenPadding.put(child, p);
+ computePadding(child, p);
+ }
+ }
+
+ // since the non ELEMENT_NODE children were filtered out, count must be updated.
+ count = childrenPadding.size();
+
+ // now combine/compare based on the parent.
+ if (count == 1) {
+ int[] p = childrenPadding.get(childrenPadding.keySet().iterator().next());
+ padding[0] = p[0];
+ padding[1] = p[1];
+ } else {
+ if ("LinearLayout".equals(localName)) { //$NON-NLS-1$
+ String orientation = getAttribute(view, "orientation", null); //$NON-NLS-1$
+
+ // default value is horizontal
+ boolean horizontal = orientation == null ||
+ "horizontal".equals("vertical"); //$NON-NLS-1$ //$NON-NLS-2$
+ combineLinearLayout(childrenPadding.values(), padding, horizontal);
+ } else if ("TableLayout".equals(localName)) { //$NON-NLS-1$
+ combineLinearLayout(childrenPadding.values(), padding, false /*horizontal*/);
+ } else if ("TableRow".equals(localName)) { //$NON-NLS-1$
+ combineLinearLayout(childrenPadding.values(), padding, true /*true*/);
+ // TODO: properly support Relative Layouts.
+// } else if ("RelativeLayout".equals(localName)) { //$NON-NLS-1$
+// combineRelativeLayout(childrenPadding, padding);
+ } else {
+ // unknown layout. For now, let's consider it's better to add the children
+ // margins in both dimensions than not at all.
+ for (int[] p : childrenPadding.values()) {
+ padding[0] += p[0];
+ padding[1] += p[1];
+ }
+ }
+ }
+ }
+
+ // if the view itself is a layout, add its padding
+ if (mLayoutNames.contains(localName)) {
+ padding[0]++;
+ padding[1]++;
+ }
+ }
+
+ /**
+ * Combines the padding of the children of a linear layout.
+ * <p/>For this layout, the padding of the children are added in the direction of
+ * the layout, while the max is taken for the other direction.
+ * @param paddings the list of the padding for the children.
+ * @param resultPadding the result padding array to fill.
+ * @param horizontal whether this layout is horizontal (<code>true</code>) or vertical
+ * (<code>false</code>)
+ */
+ private void combineLinearLayout(Collection<int[]> paddings, int[] resultPadding,
+ boolean horizontal) {
+ // The way the children are combined will depend on the direction.
+ // For instance in a vertical layout, we add the y padding as they all add to the length
+ // of the needed canvas, while we take the biggest x padding needed by the children
+
+ // the axis in which we take the sum of the padding of the children
+ int sumIndex = horizontal ? 0 : 1;
+ // the axis in which we take the max of the padding of the children
+ int maxIndex = horizontal ? 1 : 0;
+
+ int max = -1;
+ for (int[] p : paddings) {
+ resultPadding[sumIndex] += p[sumIndex];
+ if (max == -1 || max < p[maxIndex]) {
+ max = p[maxIndex];
+ }
+ }
+ resultPadding[maxIndex] = max;
+ }
+
+ /**
+ * Combine the padding of children of a relative layout.
+ * @param childrenPadding a map of the children. This is guaranteed that the node object
+ * are of type ELEMENT_NODE
+ * @param padding
+ *
+ * TODO: Not used yet. Still need (lots of) work.
+ */
+ private void combineRelativeLayout(Map<Node, int[]> childrenPadding, int[] padding) {
+ /*
+ * Combines the children of the layout.
+ * The way this works: for each children, for each direction, look for all the chidrens
+ * connected and compute the combined margin in that direction.
+ *
+ * There's a chance the returned value will be too much. this is due to the layout sometimes
+ * dropping views which will not be dropped here. It's ok, as it's better to have too
+ * much than not enough.
+ * We could fix this by matching those UiElementNode with their bounds as returned
+ * by the rendering (ie if bounds is 0/0 in h/w, then ignore the child)
+ */
+
+ // list of the UiElementNode
+ Set<Node> nodeSet = childrenPadding.keySet();
+ // map of Id -> node
+ Map<String, Node> idNodeMap = computeIdNodeMap(nodeSet);
+
+ for (Entry<Node, int[]> entry : childrenPadding.entrySet()) {
+ Node node = entry.getKey();
+
+ // first horizontal, to the left.
+ int[] leftResult = getBiggestMarginInDirection(node, 0 /*horizontal*/,
+ "layout_toRightOf", "layout_toLeftOf", //$NON-NLS-1$ //$NON-NLS-2$
+ childrenPadding, nodeSet, idNodeMap,
+ false /*includeThisPadding*/);
+
+ // then to the right
+ int[] rightResult = getBiggestMarginInDirection(node, 0 /*horizontal*/,
+ "layout_toLeftOf", "layout_toRightOf", //$NON-NLS-1$ //$NON-NLS-2$
+ childrenPadding, nodeSet, idNodeMap,
+ false /*includeThisPadding*/);
+
+ // compute total horizontal margins
+ int[] thisPadding = childrenPadding.get(node);
+ int combinedMargin =
+ (thisPadding != null ? thisPadding[0] : 0) +
+ (leftResult != null ? leftResult[0] : 0) +
+ (rightResult != null ? rightResult[0] : 0);
+ if (combinedMargin > padding[0]) {
+ padding[0] = combinedMargin;
+ }
+
+ // first vertical, above.
+ int[] topResult = getBiggestMarginInDirection(node, 1 /*horizontal*/,
+ "layout_below", "layout_above", //$NON-NLS-1$ //$NON-NLS-2$
+ childrenPadding, nodeSet, idNodeMap,
+ false /*includeThisPadding*/);
+
+ // then below
+ int[] bottomResult = getBiggestMarginInDirection(node, 1 /*horizontal*/,
+ "layout_above", "layout_below", //$NON-NLS-1$ //$NON-NLS-2$
+ childrenPadding, nodeSet, idNodeMap,
+ false /*includeThisPadding*/);
+
+ // compute total horizontal margins
+ combinedMargin =
+ (thisPadding != null ? thisPadding[1] : 0) +
+ (topResult != null ? topResult[1] : 0) +
+ (bottomResult != null ? bottomResult[1] : 0);
+ if (combinedMargin > padding[1]) {
+ padding[1] = combinedMargin;
+ }
+ }
+ }
+
+ /**
+ * Computes the biggest margin in a given direction.
+ *
+ * TODO: Not used yet. Still need (lots of) work.
+ */
+ private int[] getBiggestMarginInDirection(Node node, int resIndex, String relativeTo,
+ String inverseRelation, Map<Node, int[]> childrenPadding,
+ Set<Node> nodeSet, Map<String, Node> idNodeMap,
+ boolean includeThisPadding) {
+ NamedNodeMap attributes = node.getAttributes();
+
+ String viewId = getAttribute(node, "id", attributes); //$NON-NLS-1$
+
+ // first get the item this one is positioned relative to.
+ String toLeftOfRef = getAttribute(node, relativeTo, attributes);
+ Node toLeftOf = null;
+ if (toLeftOfRef != null) {
+ toLeftOf = idNodeMap.get(cleanUpIdReference(toLeftOfRef));
+ }
+
+ ArrayList<Node> list = null;
+ if (viewId != null) {
+ // now to the left for items being placed to the left of this one.
+ list = getMatchingNode(nodeSet, cleanUpIdReference(viewId), inverseRelation);
+ }
+
+ // now process each children in the same direction.
+ if (toLeftOf != null) {
+ if (list == null) {
+ list = new ArrayList<Node>();
+ }
+
+ if (list.indexOf(toLeftOf) == -1) {
+ list.add(toLeftOf);
+ }
+ }
+
+ int[] thisPadding = childrenPadding.get(node);
+
+ if (list != null) {
+ // since there's a combination to do, we'll return a new result object
+ int[] result = null;
+ for (Node nodeOnLeft : list) {
+ int[] tempRes = getBiggestMarginInDirection(nodeOnLeft, resIndex, relativeTo,
+ inverseRelation, childrenPadding, nodeSet, idNodeMap, true);
+ if (tempRes != null && (result == null || result[resIndex] < tempRes[resIndex])) {
+ result = tempRes;
+ }
+ }
+
+ // return the combined padding
+ if (includeThisPadding == false || thisPadding[resIndex] == 0) {
+ // just return the one we got since this object adds no padding (or doesn't
+ // need to be comibined)
+ return result;
+ } else if (result != null) { // if result is null, the main return below is used.
+ // add the result we got with the padding from the current node
+ int[] realRes = new int [2];
+ realRes[resIndex] = thisPadding[resIndex] + result[resIndex];
+ return realRes;
+ }
+ }
+
+ // if we reach this, there were no other views to the left of this one, so just return
+ // the view padding.
+ return includeThisPadding ? thisPadding : null;
+ }
+
+ /**
+ * Computes and returns a map of (id, node) for each node of a given {@link Set}.
+ * <p/>
+ * Nodes with no id are ignored and not put in the map.
+ * @param nodes the nodes to fill the map with.
+ * @return a newly allocated, non-null, map of (id, node)
+ */
+ private Map<String, Node> computeIdNodeMap(Set<Node> nodes) {
+ Map<String, Node> map = new HashMap<String, Node>();
+ for (Node node : nodes) {
+ String viewId = getAttribute(node, "id", null); //$NON-NLS-1$
+ if (viewId != null) {
+ map.put(cleanUpIdReference(viewId), node);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Cleans up a reference to an ID to return the ID itself only.
+ * @param reference the reference to "clean up".
+ * @return the id string only.
+ */
+ private String cleanUpIdReference(String reference) {
+ // format is @id/foo or @+id/foo or @android:id/foo, or something similar.
+ int slash = reference.indexOf('/');
+ return reference.substring(slash);
+ }
+
+ /**
+ * Returns a list of nodes for which a given attribute contains a reference to a given ID.
+ *
+ * @param nodes the list of nodes to search through
+ * @param resId the requested ID
+ * @param attribute the name of the attribute to test.
+ * @return a newly allocated, non-null, list of nodes. Could be empty.
+ */
+ private ArrayList<Node> getMatchingNode(Set<Node> nodes, String resId,
+ String attribute) {
+ ArrayList<Node> list = new ArrayList<Node>();
+
+ for (Node node : nodes) {
+ String value = getAttribute(node, attribute, null);
+ if (value != null) {
+ value = cleanUpIdReference(value);
+ if (value.equals(resId)) {
+ list.add(node);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * Returns an attribute for a given node.
+ * @param node the node to query
+ * @param name the name of an attribute
+ * @param attributes the option {@link NamedNodeMap} object to use to read the attributes from.
+ */
+ private static String getAttribute(Node node, String name, NamedNodeMap attributes) {
+ if (attributes == null) {
+ attributes = node.getAttributes();
+ }
+
+ if (attributes != null) {
+ Node attribute = attributes.getNamedItemNS(SdkConstants.NS_RESOURCES, name);
+ if (attribute != null) {
+ return attribute.getNodeValue();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java
new file mode 100644
index 000000000..99549ab89
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.CLASS_ACTIVITY;
+import static com.android.SdkConstants.CLASS_FRAGMENT;
+import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
+import static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CustomViewFinder;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.google.common.collect.Lists;
+import com.google.common.collect.ObjectArrays;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Content Assist Processor for /res/layout XML files
+ */
+@VisibleForTesting
+public final class LayoutContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for LayoutContentAssist
+ */
+ public LayoutContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_LAYOUT);
+ }
+
+ @Override
+ protected Object[] getChoicesForElement(String parent, Node currentNode) {
+ Object[] choices = super.getChoicesForElement(parent, currentNode);
+ if (choices == null) {
+ if (currentNode.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
+ String parentName = currentNode.getParentNode().getNodeName();
+ if (parentName.indexOf('.') != -1) {
+ // Custom view with unknown children; just use the root descriptor
+ // to get all eligible views instead
+ ElementDescriptor[] children = getRootDescriptor().getChildren();
+ for (ElementDescriptor e : children) {
+ if (e.getXmlName().startsWith(parent)) {
+ return sort(children);
+ }
+ }
+ }
+ }
+ }
+
+ if (choices == null && parent.length() >= 1 && Character.isLowerCase(parent.charAt(0))) {
+ // Custom view prefix?
+ List<ElementDescriptor> descriptors = getCustomViews();
+ if (descriptors != null && !descriptors.isEmpty()) {
+ List<ElementDescriptor> matches = Lists.newArrayList();
+ for (ElementDescriptor descriptor : descriptors) {
+ if (descriptor.getXmlLocalName().startsWith(parent)) {
+ matches.add(descriptor);
+ }
+ }
+ if (!matches.isEmpty()) {
+ return matches.toArray(new ElementDescriptor[matches.size()]);
+ }
+ }
+ }
+
+ return choices;
+ }
+
+ @Override
+ protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) {
+ ElementDescriptor[] choices = super.getElementChoicesForTextNode(parentNode);
+
+ // Add in custom views, if any
+ List<ElementDescriptor> descriptors = getCustomViews();
+ if (descriptors != null && !descriptors.isEmpty()) {
+ ElementDescriptor[] array = descriptors.toArray(
+ new ElementDescriptor[descriptors.size()]);
+ choices = ObjectArrays.concat(choices, array, ElementDescriptor.class);
+ choices = sort(choices);
+ }
+
+ return choices;
+ }
+
+ @Nullable
+ private List<ElementDescriptor> getCustomViews() {
+ // Add in custom views, if any
+ IProject project = mEditor.getProject();
+ CustomViewFinder finder = CustomViewFinder.get(project);
+ Collection<String> views = finder.getAllViews();
+ if (views == null) {
+ finder.refresh();
+ views = finder.getAllViews();
+ }
+ if (views != null && !views.isEmpty()) {
+ List<ElementDescriptor> descriptors = Lists.newArrayListWithExpectedSize(views.size());
+ CustomViewDescriptorService customViews = CustomViewDescriptorService.getInstance();
+ for (String fqcn : views) {
+ ViewElementDescriptor descriptor = customViews.getDescriptor(project, fqcn);
+ if (descriptor != null) {
+ descriptors.add(descriptor);
+ }
+ }
+
+ return descriptors;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset,
+ String parentTagName, String attributeName, Node node, String wordPrefix,
+ boolean skipEndTag, int replaceLength) {
+ super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node,
+ wordPrefix, skipEndTag, replaceLength);
+
+ boolean projectOnly = false;
+ List<String> superClasses = null;
+ if (VIEW_FRAGMENT.equals(parentTagName) && (attributeName.endsWith(ATTR_NAME)
+ || attributeName.equals(ATTR_CLASS))) {
+ // Insert fragment class matches
+ superClasses = Arrays.asList(CLASS_V4_FRAGMENT, CLASS_FRAGMENT);
+ } else if (VIEW_TAG.equals(parentTagName) && attributeName.endsWith(ATTR_CLASS)) {
+ // Insert custom view matches
+ superClasses = Collections.singletonList(CLASS_VIEW);
+ projectOnly = true;
+ } else if (attributeName.endsWith(ATTR_CONTEXT)) {
+ // Insert activity matches
+ superClasses = Collections.singletonList(CLASS_ACTIVITY);
+ }
+
+ if (superClasses != null) {
+ IProject project = mEditor.getProject();
+ if (project == null) {
+ return false;
+ }
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ IType type = javaProject.findType(superClasses.get(0));
+ Set<IType> elements = new HashSet<IType>();
+ if (type != null) {
+ ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor());
+ IType[] allSubtypes = hierarchy.getAllSubtypes(type);
+ for (IType subType : allSubtypes) {
+ if (!projectOnly || subType.getResource() != null) {
+ elements.add(subType);
+ }
+ }
+ }
+ assert superClasses.size() <= 2; // If more, need to do additional work below
+ if (superClasses.size() == 2) {
+ type = javaProject.findType(superClasses.get(1));
+ if (type != null) {
+ ITypeHierarchy hierarchy = type.newTypeHierarchy(
+ new NullProgressMonitor());
+ IType[] allSubtypes = hierarchy.getAllSubtypes(type);
+ for (IType subType : allSubtypes) {
+ if (!projectOnly || subType.getResource() != null) {
+ elements.add(subType);
+ }
+ }
+ }
+ }
+
+ List<IType> sorted = new ArrayList<IType>(elements);
+ Collections.sort(sorted, new Comparator<IType>() {
+ @Override
+ public int compare(IType type1, IType type2) {
+ String fqcn1 = type1.getFullyQualifiedName();
+ String fqcn2 = type2.getFullyQualifiedName();
+ int category1 = fqcn1.startsWith(ANDROID_PKG_PREFIX) ? 1 : -1;
+ int category2 = fqcn2.startsWith(ANDROID_PKG_PREFIX) ? 1 : -1;
+ if (category1 != category2) {
+ return category1 - category2;
+ }
+ return fqcn1.compareTo(fqcn2);
+ }
+ });
+ addMatchingProposals(proposals, sorted.toArray(), offset, node, wordPrefix,
+ (char) 0, false /* isAttribute */, false /* isNew */,
+ false /* skipEndTag */, replaceLength);
+ return true;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java
new file mode 100644
index 000000000..1015d7d86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java
@@ -0,0 +1,1001 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.annotations.VisibleForTesting.Visibility;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.XmlEditorMultiOutline;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutActionBar;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceFolderType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.tools.lint.client.api.IssueRegistry;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.jobs.JobChangeAdapter;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.ISelectionService;
+import org.eclipse.ui.IShowEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Multi-page form editor for /res/layout XML files.
+ */
+public class LayoutEditorDelegate extends CommonXmlDelegate
+ implements IShowEditorInput, CommonXmlDelegate.IActionContributorDelegate {
+
+ /** The prefix for layout folders that are not the default layout folder */
+ private static final String LAYOUT_FOLDER_PREFIX = "layout-"; //$NON-NLS-1$
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public LayoutEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.LAYOUT == type) {
+ return new LayoutEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ AdtConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiDocumentNode mUiDocRootNode;
+
+ private GraphicalEditorPart mGraphicalEditor;
+ private int mGraphicalEditorIndex;
+
+ /** Implementation of the {@link IContentOutlinePage} for this editor */
+ private OutlinePage mLayoutOutline;
+
+ /** The XML editor outline */
+ private IContentOutlinePage mEditorOutline;
+
+ /** Multiplexing outline, used for multi-page editors that have their own outline */
+ private XmlEditorMultiOutline mMultiOutline;
+
+ /**
+ * Temporary flag set by the editor caret listener which is used to cause
+ * the next getAdapter(IContentOutlinePage.class) call to return the editor
+ * outline rather than the multi-outline. See the {@link #delegateGetAdapter}
+ * method for details.
+ */
+ private boolean mCheckOutlineAdapter;
+
+ /** Custom implementation of {@link IPropertySheetPage} for this editor */
+ private IPropertySheetPage mPropertyPage;
+
+ private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap =
+ new HashMap<String, ElementDescriptor>();
+
+ private EclipseLintClient mClient;
+
+ /**
+ * Flag indicating if the replacement file is due to a config change.
+ * If false, it means the new file is due to an "open action" from the user.
+ */
+ private boolean mNewFileOnConfigChange = false;
+
+ /**
+ * Checks whether an editor part is an instance of {@link CommonXmlEditor}
+ * with an associated {@link LayoutEditorDelegate} delegate.
+ *
+ * @param editorPart An editor part. Can be null.
+ * @return The {@link LayoutEditorDelegate} delegate associated with the editor or null.
+ */
+ public static @Nullable LayoutEditorDelegate fromEditor(@Nullable IEditorPart editorPart) {
+ if (editorPart instanceof CommonXmlEditor) {
+ CommonXmlDelegate delegate = ((CommonXmlEditor) editorPart).getDelegate();
+ if (delegate instanceof LayoutEditorDelegate) {
+ return ((LayoutEditorDelegate) delegate);
+ }
+ } else if (editorPart instanceof GraphicalEditorPart) {
+ GraphicalEditorPart part = (GraphicalEditorPart) editorPart;
+ return part.getEditorDelegate();
+ }
+ return null;
+ }
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected LayoutEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new LayoutContentAssist());
+ // Note that LayoutEditor has its own listeners and does not
+ // need to call editor.addDefaultTargetListener().
+ }
+
+ /**
+ * Returns the {@link RulesEngine} associated with this editor
+ *
+ * @return the {@link RulesEngine} associated with this editor.
+ */
+ public RulesEngine getRulesEngine() {
+ return mGraphicalEditor.getRulesEngine();
+ }
+
+ /**
+ * Returns the {@link GraphicalEditorPart} associated with this editor
+ *
+ * @return the {@link GraphicalEditorPart} associated with this editor
+ */
+ public GraphicalEditorPart getGraphicalEditor() {
+ return mGraphicalEditor;
+ }
+
+ /**
+ * @return The root node of the UI element hierarchy
+ */
+ @Override
+ public UiDocumentNode getUiRootNode() {
+ return mUiDocRootNode;
+ }
+
+ public void setNewFileOnConfigChange(boolean state) {
+ mNewFileOnConfigChange = state;
+ }
+
+ // ---- Base Class Overrides ----
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.dispose();
+ mGraphicalEditor = null;
+ }
+ }
+
+ /**
+ * Save the XML.
+ * <p/>
+ * Clients must NOT call this directly. Instead they should always
+ * call {@link CommonXmlEditor#doSave(IProgressMonitor)} so that th
+ * editor super class can commit the data properly.
+ * <p/>
+ * Here we just need to tell the graphical editor that the model has
+ * been saved.
+ */
+ @Override
+ public void delegateDoSave(IProgressMonitor monitor) {
+ super.delegateDoSave(monitor);
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.doSave(monitor);
+ }
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ public void delegateCreateFormPages() {
+ try {
+ // get the file being edited so that it can be passed to the layout editor.
+ IFile editedFile = null;
+ IEditorInput input = getEditor().getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ editedFile = fileInput.getFile();
+ if (!editedFile.isAccessible()) {
+ return;
+ }
+ } else {
+ AdtPlugin.log(IStatus.ERROR,
+ "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$
+ input.toString());
+ }
+
+ // It is possible that the Layout Editor already exits if a different version
+ // of the same layout is being opened (either through "open" action from
+ // the user, or through a configuration change in the configuration selector.)
+ if (mGraphicalEditor == null) {
+
+ // Instantiate GLE v2
+ mGraphicalEditor = new GraphicalEditorPart(this);
+
+ mGraphicalEditorIndex = getEditor().addPage(mGraphicalEditor,
+ getEditor().getEditorInput());
+ getEditor().setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
+
+ mGraphicalEditor.openFile(editedFile);
+ } else {
+ if (mNewFileOnConfigChange) {
+ mGraphicalEditor.changeFileOnNewConfig(editedFile);
+ mNewFileOnConfigChange = false;
+ } else {
+ mGraphicalEditor.replaceFile(editedFile);
+ }
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public void delegatePostCreatePages() {
+ // Optional: set the default page. Eventually a default page might be
+ // restored by selectDefaultPage() later based on the last page used by the user.
+ // For example, to make the last page the default one (rather than the first page),
+ // uncomment this line:
+ // setActivePage(getPageCount() - 1);
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the name of the layout.
+ */
+ @Override
+ public void delegateSetInput(IEditorInput input) {
+ handleNewInput(input);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
+ */
+ public void delegateSetInputWithNotify(IEditorInput input) {
+ handleNewInput(input);
+ }
+
+ /**
+ * Called to replace the current {@link IEditorInput} with another one.
+ * <p/>
+ * This is used when {@link LayoutEditorMatchingStrategy} returned
+ * <code>true</code> which means we're opening a different configuration of
+ * the same layout.
+ */
+ @Override
+ public void showEditorInput(IEditorInput editorInput) {
+ if (getEditor().getEditorInput().equals(editorInput)) {
+ return;
+ }
+
+ // Save the current editor input. This must be called on the editor itself
+ // since it's the base editor that commits pending changes.
+ getEditor().doSave(new NullProgressMonitor());
+
+ // Get the current page
+ int currentPage = getEditor().getActivePage();
+
+ // Remove the pages, except for the graphical editor, which will be dynamically adapted
+ // to the new model.
+ // page after the graphical editor:
+ int count = getEditor().getPageCount();
+ for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
+ getEditor().removePage(i);
+ }
+ // Pages before the graphical editor
+ for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
+ getEditor().removePage(i);
+ }
+
+ // Set the current input. We're in the delegate, the input must
+ // be set into the actual editor instance.
+ getEditor().setInputWithNotify(editorInput);
+
+ // Re-create or reload the pages with the default page shown as the previous active page.
+ getEditor().createAndroidPages();
+ getEditor().selectDefaultPage(Integer.toString(currentPage));
+
+ // When changing an input file of an the editor, the titlebar is not refreshed to
+ // show the new path/to/file being edited. So we force a refresh
+ getEditor().firePropertyChange(IWorkbenchPart.PROP_TITLE);
+ }
+
+ /** Performs a complete refresh of the XML model */
+ public void refreshXmlModel() {
+ Document xmlDoc = mUiDocRootNode.getXmlDocument();
+
+ delegateInitUiRootNode(true /*force*/);
+ mUiDocRootNode.loadFromXmlNode(xmlDoc);
+
+ // Update the model first, since it is used by the viewers.
+ // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's
+ // a no-op. Instead call onXmlModelChanged on the graphical editor.
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onXmlModelChanged();
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ public void delegateXmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ delegateInitUiRootNode(false /*force*/);
+
+ mUiDocRootNode.loadFromXmlNode(xml_doc);
+
+ // Update the model first, since it is used by the viewers.
+ // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's
+ // a no-op. Instead call onXmlModelChanged on the graphical editor.
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onXmlModelChanged();
+ }
+ }
+
+ /**
+ * Tells the graphical editor to recompute its layout.
+ */
+ public void recomputeLayout() {
+ mGraphicalEditor.recomputeLayout();
+ }
+
+ /**
+ * Does this editor participate in the "format GUI editor changes" option?
+ *
+ * @return true since this editor supports automatically formatting XML
+ * affected by GUI changes
+ */
+ @Override
+ public boolean delegateSupportsFormatOnGuiEdit() {
+ return true;
+ }
+
+ /**
+ * Returns one of the issues for the given node (there could be more than one)
+ *
+ * @param node the node to look up lint issues for
+ * @return the marker for one of the issues found for the given node
+ */
+ @Nullable
+ public IMarker getIssueForNode(@Nullable UiViewElementNode node) {
+ if (node == null) {
+ return null;
+ }
+
+ if (mClient != null) {
+ return mClient.getIssueForNode(node);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a collection of nodes that have one or more lint warnings
+ * associated with them (retrievable via
+ * {@link #getIssueForNode(UiViewElementNode)})
+ *
+ * @return a collection of nodes, which should <b>not</b> be modified by the
+ * caller
+ */
+ @Nullable
+ public Collection<Node> getLintNodes() {
+ if (mClient != null) {
+ return mClient.getIssueNodes();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Job delegateRunLint() {
+ // We want to customize the {@link EclipseLintClient} created to run this
+ // single file lint, in particular such that we can set the mode which collects
+ // nodes on that lint job, such that we can quickly look up error nodes
+ //Job job = super.delegateRunLint();
+
+ Job job = null;
+ IFile file = getEditor().getInputFile();
+ if (file != null) {
+ IssueRegistry registry = EclipseLintClient.getRegistry();
+ List<IFile> resources = Collections.singletonList(file);
+ mClient = new EclipseLintClient(registry,
+ resources, getEditor().getStructuredDocument(), false /*fatal*/);
+
+ mClient.setCollectNodes(true);
+
+ job = EclipseLintRunner.startLint(mClient, resources, file,
+ false /*show*/);
+ }
+
+ if (job != null) {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ if (graphicalEditor != null) {
+ job.addJobChangeListener(new LintJobListener(graphicalEditor));
+ }
+ }
+ return job;
+ }
+
+ private class LintJobListener extends JobChangeAdapter implements Runnable {
+ private final GraphicalEditorPart mEditor;
+ private final LayoutCanvas mCanvas;
+
+ LintJobListener(GraphicalEditorPart editor) {
+ mEditor = editor;
+ mCanvas = editor.getCanvasControl();
+ }
+
+ @Override
+ public void done(IJobChangeEvent event) {
+ LayoutActionBar bar = mEditor.getLayoutActionBar();
+ if (!bar.isDisposed()) {
+ bar.updateErrorIndicator();
+ }
+
+ // Redraw
+ if (!mCanvas.isDisposed()) {
+ mCanvas.getDisplay().asyncExec(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!mCanvas.isDisposed()) {
+ mCanvas.redraw();
+
+ OutlinePage outlinePage = mCanvas.getOutlinePage();
+ if (outlinePage != null) {
+ outlinePage.refreshIcons();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
+ */
+ @Override
+ public Object delegateGetAdapter(Class<?> adapter) {
+ if (adapter == IContentOutlinePage.class) {
+ // Somebody has requested the outline. Eclipse can only have a single outline page,
+ // even for a multi-part editor:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
+ // To work around this we use PDE's workaround of having a single multiplexing
+ // outline which switches its contents between the outline pages we register
+ // for it, and then on page switch we notify it to update itself.
+
+ // There is one complication: The XML editor outline listens for the editor
+ // selection and uses this to automatically expand its tree children and show
+ // the current node containing the caret as selected. Unfortunately, this
+ // listener code contains this:
+ //
+ // /* Bug 136310, unless this page is that part's
+ // * IContentOutlinePage, ignore the selection change */
+ // if (part.getAdapter(IContentOutlinePage.class) == this) {
+ //
+ // This means that when we return the multiplexing outline from this getAdapter
+ // method, the outline no longer updates to track the selection.
+ // To work around this, we use the following hack^H^H^H^H technique:
+ // - Add a selection listener *before* requesting the editor outline, such
+ // that the selection listener is told about the impending selection event
+ // right before the editor outline hears about it. Set the flag
+ // mCheckOutlineAdapter to true. (We also only set it if the editor view
+ // itself is active.)
+ // - In this getAdapter method, when somebody requests the IContentOutline.class,
+ // see if mCheckOutlineAdapter to see if this request is *likely* coming
+ // from the XML editor outline. If so, make sure it is by actually looking
+ // at the signature of the caller. If it's the editor outline, then return
+ // the editor outline instance itself rather than the multiplexing outline.
+ if (mCheckOutlineAdapter && mEditorOutline != null) {
+ mCheckOutlineAdapter = false;
+ // Make *sure* this is really the editor outline calling in case
+ // future versions of Eclipse changes the sequencing or dispatch of selection
+ // events:
+ StackTraceElement[] frames = new Throwable().fillInStackTrace().getStackTrace();
+ if (frames.length > 2) {
+ StackTraceElement frame = frames[2];
+ if (frame.getClassName().equals(
+ "org.eclipse.wst.sse.ui.internal.contentoutline." + //$NON-NLS-1$
+ "ConfigurableContentOutlinePage$PostSelectionServiceListener")) { //$NON-NLS-1$
+ return mEditorOutline;
+ }
+ }
+ }
+
+ // Use a multiplexing outline: workaround for
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
+ if (mMultiOutline == null || mMultiOutline.isDisposed()) {
+ mMultiOutline = new XmlEditorMultiOutline();
+ mMultiOutline.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ getEditor().getSite().getSelectionProvider().setSelection(selection);
+ if (getEditor().getIgnoreXmlUpdate()) {
+ return;
+ }
+ SelectionManager manager =
+ mGraphicalEditor.getCanvasControl().getSelectionManager();
+ manager.setSelection(selection);
+ }
+ });
+ updateOutline(getEditor().getActivePageInstance());
+ }
+
+ return mMultiOutline;
+ }
+
+ if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
+ if (mPropertyPage == null) {
+ mPropertyPage = new PropertySheetPage(mGraphicalEditor);
+ }
+
+ return mPropertyPage;
+ }
+
+ // return default
+ return super.delegateGetAdapter(adapter);
+ }
+
+ /**
+ * Update the contents of the outline to show either the XML editor outline
+ * or the layout editor graphical outline depending on which tab is visible
+ */
+ private void updateOutline(IFormPage page) {
+ if (mMultiOutline == null) {
+ return;
+ }
+
+ IContentOutlinePage outline;
+ CommonXmlEditor editor = getEditor();
+ if (!editor.isEditorPageActive()) {
+ outline = getGraphicalOutline();
+ } else {
+ // Use plain XML editor outline instead
+ if (mEditorOutline == null) {
+ StructuredTextEditor structuredTextEditor = editor.getStructuredTextEditor();
+ if (structuredTextEditor != null) {
+ IWorkbenchWindow window = editor.getSite().getWorkbenchWindow();
+ ISelectionService service = window.getSelectionService();
+ service.addPostSelectionListener(new ISelectionListener() {
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ if (getEditor().isEditorPageActive()) {
+ mCheckOutlineAdapter = true;
+ }
+ }
+ });
+
+ mEditorOutline = (IContentOutlinePage) structuredTextEditor.getAdapter(
+ IContentOutlinePage.class);
+ }
+ }
+
+ outline = mEditorOutline;
+ }
+
+ mMultiOutline.setPageActive(outline);
+ }
+
+ /**
+ * Returns the graphical outline associated with the layout editor
+ *
+ * @return the outline page, never null
+ */
+ @NonNull
+ public OutlinePage getGraphicalOutline() {
+ if (mLayoutOutline == null) {
+ mLayoutOutline = new OutlinePage(mGraphicalEditor);
+ }
+
+ return mLayoutOutline;
+ }
+
+ @Override
+ public void delegatePageChange(int newPageIndex) {
+ if (getEditor().getCurrentPage() == getEditor().getTextPageIndex() &&
+ newPageIndex == mGraphicalEditorIndex) {
+ // You're switching from the XML editor to the WYSIWYG editor;
+ // look at the caret position and figure out which node it corresponds to
+ // (if any) and if found, select the corresponding visual element.
+ ISourceViewer textViewer = getEditor().getStructuredSourceViewer();
+ int caretOffset = textViewer.getTextWidget().getCaretOffset();
+ if (caretOffset >= 0) {
+ Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset);
+ if (node != null && mGraphicalEditor != null) {
+ mGraphicalEditor.select(node);
+ }
+ }
+ }
+
+ super.delegatePageChange(newPageIndex);
+
+ if (mGraphicalEditor != null) {
+ if (newPageIndex == mGraphicalEditorIndex) {
+ mGraphicalEditor.activated();
+ } else {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+
+ @Override
+ public int delegateGetPersistenceCategory() {
+ return AndroidXmlEditor.CATEGORY_LAYOUT;
+ }
+
+ @Override
+ public void delegatePostPageChange(int newPageIndex) {
+ super.delegatePostPageChange(newPageIndex);
+
+ if (mGraphicalEditor != null) {
+ LayoutCanvas canvas = mGraphicalEditor.getCanvasControl();
+ if (canvas != null) {
+ IActionBars bars = getEditor().getEditorSite().getActionBars();
+ if (bars != null) {
+ canvas.updateGlobalActions(bars);
+ }
+ }
+ }
+
+ IFormPage page = getEditor().getActivePageInstance();
+ updateOutline(page);
+ }
+
+ @Override
+ public IFormPage delegatePostSetActivePage(IFormPage superReturned, String pageIndex) {
+ IFormPage page = superReturned;
+ if (page != null) {
+ updateOutline(page);
+ }
+
+ return page;
+ }
+
+ // ----- IActionContributorDelegate methods ----
+
+ @Override
+ public void setActiveEditor(IEditorPart part, IActionBars bars) {
+ if (mGraphicalEditor != null) {
+ LayoutCanvas canvas = mGraphicalEditor.getCanvasControl();
+ if (canvas != null) {
+ canvas.updateGlobalActions(bars);
+ }
+ }
+ }
+
+
+ @Override
+ public void delegateActivated() {
+ if (mGraphicalEditor != null) {
+ if (getEditor().getActivePage() == mGraphicalEditorIndex) {
+ mGraphicalEditor.activated();
+ } else {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+
+ @Override
+ public void delegateDeactivated() {
+ if (mGraphicalEditor != null && getEditor().getActivePage() == mGraphicalEditorIndex) {
+ mGraphicalEditor.deactivated();
+ }
+ }
+
+ @Override
+ public String delegateGetPartName() {
+ IEditorInput editorInput = getEditor().getEditorInput();
+ if (!AdtPrefs.getPrefs().isSharedLayoutEditor()
+ && editorInput instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput) editorInput;
+ IFile file = fileInput.getFile();
+ IContainer parent = file.getParent();
+ if (parent != null) {
+ String parentName = parent.getName();
+ if (parentName.startsWith(LAYOUT_FOLDER_PREFIX)) {
+ parentName = parentName.substring(LAYOUT_FOLDER_PREFIX.length());
+ return parentName + File.separatorChar + file.getName();
+ }
+ }
+ }
+
+ return super.delegateGetPartName();
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Returns true if the Graphics editor page is visible. This <b>must</b> be
+ * called from the UI thread.
+ */
+ public boolean isGraphicalEditorActive() {
+ IWorkbenchPartSite workbenchSite = getEditor().getSite();
+ IWorkbenchPage workbenchPage = workbenchSite.getPage();
+
+ // check if the editor is visible in the workbench page
+ if (workbenchPage.isPartVisible(getEditor())
+ && workbenchPage.getActiveEditor() == getEditor()) {
+ // and then if the page of the editor is visible (not to be confused with
+ // the workbench page)
+ return mGraphicalEditorIndex == getEditor().getActivePage();
+ }
+
+ return false;
+ }
+
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (mUiDocRootNode == null || force) {
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getEditor().getTargetData();
+
+ Document doc = null;
+ if (mUiDocRootNode != null) {
+ doc = mUiDocRootNode.getXmlDocument();
+ }
+
+ DocumentDescriptor desc;
+ if (data == null) {
+ desc = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getLayoutDescriptors().getDescriptor();
+ }
+
+ // get the descriptors from the data.
+ mUiDocRootNode = (UiDocumentNode) desc.createUiNode();
+ super.setUiRootNode(mUiDocRootNode);
+ mUiDocRootNode.setEditor(getEditor());
+
+ mUiDocRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() {
+ @Override
+ public ElementDescriptor getDescriptor(String xmlLocalName) {
+ ElementDescriptor unknown = mUnknownDescriptorMap.get(xmlLocalName);
+ if (unknown == null) {
+ unknown = createUnknownDescriptor(xmlLocalName);
+ mUnknownDescriptorMap.put(xmlLocalName, unknown);
+ }
+
+ return unknown;
+ }
+ });
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ /**
+ * Creates a new {@link ViewElementDescriptor} for an unknown XML local name
+ * (i.e. one that was not mapped by the current descriptors).
+ * <p/>
+ * Since we deal with layouts, we returns either a descriptor for a custom view
+ * or one for the base View.
+ *
+ * @param xmlLocalName The XML local name to match.
+ * @return A non-null {@link ViewElementDescriptor}.
+ */
+ private ViewElementDescriptor createUnknownDescriptor(String xmlLocalName) {
+ ViewElementDescriptor desc = null;
+ IEditorInput editorInput = getEditor().getEditorInput();
+ if (editorInput instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput)editorInput;
+ IProject project = fileInput.getFile().getProject();
+
+ // Check if we can find a custom view specific to this project.
+ // This only works if there's an actual matching custom class in the project.
+ if (xmlLocalName.indexOf('.') != -1) {
+ desc = CustomViewDescriptorService.getInstance().getDescriptor(project,
+ xmlLocalName);
+ }
+
+ if (desc == null) {
+ // If we didn't find a custom view, create a synthetic one using the
+ // the base View descriptor as a model.
+ // This is a layout after all, so every XML node should represent
+ // a view.
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ // data can be null when the target is still loading
+ ViewElementDescriptor viewDesc =
+ data.getLayoutDescriptors().getBaseViewDescriptor();
+
+ desc = new ViewElementDescriptor(
+ xmlLocalName, // xml local name
+ xmlLocalName, // ui_name
+ xmlLocalName, // canonical class name
+ null, // tooltip
+ null, // sdk_url
+ viewDesc.getAttributes(),
+ viewDesc.getLayoutAttributes(),
+ null, // children
+ false /* mandatory */);
+ desc.setSuperClass(viewDesc);
+ }
+ }
+ }
+ }
+ }
+
+ if (desc == null) {
+ // We can only arrive here if the SDK's android target has not finished
+ // loading. Just create a dummy descriptor with no attributes to be able
+ // to continue.
+ desc = new ViewElementDescriptor(xmlLocalName, xmlLocalName);
+ }
+ return desc;
+ }
+
+ private void onDescriptorsChanged(Document document) {
+
+ mUnknownDescriptorMap.clear();
+
+ if (document != null) {
+ mUiDocRootNode.loadFromXmlNode(document);
+ } else {
+ mUiDocRootNode.reloadFromXmlNode(mUiDocRootNode.getXmlDocument());
+ }
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onTargetChange();
+ mGraphicalEditor.reloadPalette();
+ mGraphicalEditor.getCanvasControl().syncPreviewMode();
+ }
+ }
+
+ /**
+ * Handles a new input, and update the part name.
+ * @param input the new input.
+ */
+ private void handleNewInput(IEditorInput input) {
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ getEditor().setPartName(String.format("%1$s", file.getName()));
+ }
+ }
+
+ /**
+ * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN.
+ * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info.
+ */
+ public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
+ ViewElementDescriptor desc = null;
+
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data != null) {
+ LayoutDescriptors layoutDesc = data.getLayoutDescriptors();
+ if (layoutDesc != null) {
+ DocumentDescriptor docDesc = layoutDesc.getDescriptor();
+ if (docDesc != null) {
+ desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null);
+ }
+ }
+ }
+
+ if (desc == null) {
+ // We failed to find a descriptor for the given FQCN.
+ // Let's consider custom classes and create one as needed.
+ desc = createUnknownDescriptor(fqcn);
+ }
+
+ return desc;
+ }
+
+ /**
+ * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches
+ * the requested FQCN.
+ *
+ * @param fqcn The target View FQCN to find.
+ * @param descriptors A list of children descriptors to iterate through.
+ * @param visited A set we use to remember which descriptors have already been visited,
+ * necessary since the view descriptor hierarchy is cyclic.
+ * @return Either a matching {@link ViewElementDescriptor} or null.
+ */
+ private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn,
+ ElementDescriptor[] descriptors,
+ Set<ElementDescriptor> visited) {
+ if (visited == null) {
+ visited = new HashSet<ElementDescriptor>();
+ }
+
+ if (descriptors != null) {
+ for (ElementDescriptor desc : descriptors) {
+ if (visited.add(desc)) {
+ // Set.add() returns true if this a new element that was added to the set.
+ // That means we haven't visited this descriptor yet.
+ // We want a ViewElementDescriptor with a matching FQCN.
+ if (desc instanceof ViewElementDescriptor &&
+ fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) {
+ return (ViewElementDescriptor) desc;
+ }
+
+ // Visit its children
+ ViewElementDescriptor vd =
+ internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited);
+ if (vd != null) {
+ return vd;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java
new file mode 100644
index 000000000..c1c606854
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorMatchingStrategy.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorMatchingStrategy;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Matching strategy for the Layout Editor. This is used to open all configurations of a layout
+ * in the same editor.
+ */
+public class LayoutEditorMatchingStrategy implements IEditorMatchingStrategy {
+
+ @Override
+ public boolean matches(IEditorReference editorRef, IEditorInput input) {
+ // first check that the file being opened is a layout file.
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+
+ // get the IFile object and check it's in one of the layout folders.
+ IFile file = fileInput.getFile();
+ ResourceManager manager = ResourceManager.getInstance();
+ ResourceFolder resFolder = manager.getResourceFolder(file);
+
+ // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput()
+ // is expensive so try exclude files that definitely don't match, such
+ // as those with the wrong extension or wrong file name
+ if (!file.getName().equals(editorRef.getName()) ||
+ !editorRef.getId().equals(CommonXmlEditor.ID)) {
+ return false;
+ }
+
+ // if it's a layout, we now check the name of the fileInput against the name of the
+ // file being currently edited by the editor since those are independent of the config.
+ if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT) {
+ try {
+ IEditorInput editorInput = editorRef.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ FileEditorInput editorFileInput = (FileEditorInput)editorInput;
+ IFile editorFile = editorFileInput.getFile();
+
+ ResourceFolder editorFolder = manager.getResourceFolder(editorFile);
+ if (editorFolder == null
+ || editorFolder.getType() != ResourceFolderType.LAYOUT) {
+ return false;
+ }
+
+ return editorFile.getProject().equals(file.getProject())
+ && editorFile.getName().equals(file.getName());
+ }
+ } catch (PartInitException e) {
+ // we do nothing, we'll just return false.
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java
new file mode 100644
index 000000000..4e4429dc8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Monitor for file changes that could trigger a layout redraw, or a UI update
+ */
+public final class LayoutReloadMonitor {
+
+ // singleton, enforced by private constructor.
+ private final static LayoutReloadMonitor sThis = new LayoutReloadMonitor();
+
+ /**
+ * Map of listeners by IProject.
+ */
+ private final Map<IProject, List<ILayoutReloadListener>> mListenerMap =
+ new HashMap<IProject, List<ILayoutReloadListener>>();
+
+ public final static class ChangeFlags {
+ public boolean code = false;
+ /** any non-layout resource changes */
+ public boolean resources = false;
+ public boolean rClass = false;
+ public boolean localeList = false;
+ public boolean manifest = false;
+
+ boolean isAllTrue() {
+ return code && resources && rClass && localeList && manifest;
+ }
+ }
+
+ /**
+ * List of projects having received a resource change.
+ */
+ private final Map<IProject, ChangeFlags> mProjectFlags = new HashMap<IProject, ChangeFlags>();
+
+ /**
+ * Classes which implement this interface provide a method to respond to resource changes
+ * triggering a layout redraw
+ */
+ public interface ILayoutReloadListener {
+ /**
+ * Sent when the layout needs to be redrawn
+ *
+ * @param flags a {@link ChangeFlags} object indicating what type of resource changed.
+ * @param libraryModified <code>true</code> if the changeFlags are not for the project
+ * associated with the listener, but instead correspond to a library.
+ */
+ void reloadLayout(ChangeFlags flags, boolean libraryModified);
+ }
+
+ /**
+ * Returns the single instance of {@link LayoutReloadMonitor}.
+ */
+ public static LayoutReloadMonitor getMonitor() {
+ return sThis;
+ }
+
+ private LayoutReloadMonitor() {
+ // listen to resource changes. Used for non-layout resource (trigger a redraw), or
+ // any resource folder (trigger a locale list refresh)
+ ResourceManager.getInstance().addListener(mResourceListener);
+
+ // also listen for .class file changed in case the layout has custom view classes.
+ GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
+ monitor.addFileListener(mFileListener,
+ IResourceDelta.ADDED | IResourceDelta.CHANGED | IResourceDelta.REMOVED);
+
+ monitor.addResourceEventListener(mResourceEventListener);
+ }
+
+ /**
+ * Adds a listener for a given {@link IProject}.
+ * @param project
+ * @param listener
+ */
+ public void addListener(IProject project, ILayoutReloadListener listener) {
+ synchronized (mListenerMap) {
+ List<ILayoutReloadListener> list = mListenerMap.get(project);
+ if (list == null) {
+ list = new ArrayList<ILayoutReloadListener>();
+ mListenerMap.put(project, list);
+ }
+
+ list.add(listener);
+ }
+ }
+
+ /**
+ * Removes a listener for a given {@link IProject}.
+ */
+ public void removeListener(IProject project, ILayoutReloadListener listener) {
+ synchronized (mListenerMap) {
+ List<ILayoutReloadListener> list = mListenerMap.get(project);
+ if (list != null) {
+ list.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener, no matter which {@link IProject} it was associated with.
+ */
+ public void removeListener(ILayoutReloadListener listener) {
+ synchronized (mListenerMap) {
+
+ for (List<ILayoutReloadListener> list : mListenerMap.values()) {
+ Iterator<ILayoutReloadListener> it = list.iterator();
+ while (it.hasNext()) {
+ ILayoutReloadListener i = it.next();
+ if (i == listener) {
+ it.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Implementation of the {@link IFileListener} as an internal class so that the methods
+ * do not appear in the public API of {@link LayoutReloadMonitor}.
+ *
+ * This is only to detect code and manifest change. Resource changes (located in res/)
+ * is done through {@link #mResourceListener}.
+ */
+ private IFileListener mFileListener = new IFileListener() {
+ /*
+ * Callback for IFileListener. Called when a file changed.
+ * This records the changes for each project, but does not notify listeners.
+ */
+ @Override
+ public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
+ int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
+ // This listener only cares about .class files and AndroidManifest.xml files
+ if (!(SdkConstants.EXT_CLASS.equals(extension)
+ || SdkConstants.EXT_XML.equals(extension)
+ && SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()))) {
+ return;
+ }
+
+ // get the file's project
+ IProject project = file.getProject();
+
+ if (isAndroidProject) {
+ // project is an Android project, it's the one being affected
+ // directly by its own file change.
+ processFileChanged(file, project, extension);
+ } else {
+ // check the projects depending on it, if they are Android project, update them.
+ IProject[] referencingProjects = project.getReferencingProjects();
+
+ for (IProject p : referencingProjects) {
+ try {
+ boolean hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT);
+ if (hasAndroidNature) {
+ // the changed project is a dependency on an Android project,
+ // update the main project.
+ processFileChanged(file, p, extension);
+ }
+ } catch (CoreException e) {
+ // do nothing if the nature cannot be queried.
+ }
+ }
+ }
+ }
+
+ /**
+ * Processes a file change for a given project which may or may not be the file's project.
+ * @param file the changed file
+ * @param project the project impacted by the file change.
+ */
+ private void processFileChanged(IFile file, IProject project, String extension) {
+ // if this project has already been marked as modified, we do nothing.
+ ChangeFlags changeFlags = mProjectFlags.get(project);
+ if (changeFlags != null && changeFlags.isAllTrue()) {
+ return;
+ }
+
+ // here we only care about code change (so change for .class files).
+ // Resource changes is handled by the IResourceListener.
+ if (SdkConstants.EXT_CLASS.equals(extension)) {
+ if (file.getName().matches("R[\\$\\.](.*)")) {
+ // this is a R change!
+ if (changeFlags == null) {
+ changeFlags = new ChangeFlags();
+ mProjectFlags.put(project, changeFlags);
+ }
+
+ changeFlags.rClass = true;
+ } else {
+ // this is a code change!
+ if (changeFlags == null) {
+ changeFlags = new ChangeFlags();
+ mProjectFlags.put(project, changeFlags);
+ }
+
+ changeFlags.code = true;
+ }
+ } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()) &&
+ file.getParent().equals(project)) {
+ // this is a manifest change!
+ if (changeFlags == null) {
+ changeFlags = new ChangeFlags();
+ mProjectFlags.put(project, changeFlags);
+ }
+
+ changeFlags.manifest = true;
+ }
+ }
+ };
+
+ /**
+ * Implementation of the {@link IResourceEventListener} as an internal class so that the methods
+ * do not appear in the public API of {@link LayoutReloadMonitor}.
+ */
+ private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
+ /*
+ * Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a
+ * resource change event. This is called once, while fileChanged can be
+ * called several times.
+ *
+ */
+ @Override
+ public void resourceChangeEventStart() {
+ // nothing to be done here, it all happens in the resourceChangeEventEnd
+ }
+
+ /*
+ * Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource
+ * change event. This is where we notify the listeners.
+ */
+ @Override
+ public void resourceChangeEventEnd() {
+ // for each IProject that was changed, we notify all the listeners.
+ for (Entry<IProject, ChangeFlags> entry : mProjectFlags.entrySet()) {
+ IProject project = entry.getKey();
+
+ // notify the project itself.
+ notifyForProject(project, entry.getValue(), false);
+
+ // check if the project is a library, and if it is search for what other
+ // project depends on this one (directly or not)
+ ProjectState state = Sdk.getProjectState(project);
+ if (state != null && state.isLibrary()) {
+ Set<ProjectState> mainProjects = Sdk.getMainProjectsFor(project);
+ for (ProjectState mainProject : mainProjects) {
+ // always give the changeflag of the modified project.
+ notifyForProject(mainProject.getProject(), entry.getValue(), true);
+ }
+ }
+ }
+
+ // empty the list.
+ mProjectFlags.clear();
+ }
+
+ /**
+ * Notifies the listeners for a given project.
+ * @param project the project for which the listeners must be notified
+ * @param flags the change flags to pass to the listener
+ * @param libraryChanged a flag indicating if the change flags are for the give project,
+ * or if they are for a library dependency.
+ */
+ private void notifyForProject(IProject project, ChangeFlags flags,
+ boolean libraryChanged) {
+ synchronized (mListenerMap) {
+ List<ILayoutReloadListener> listeners = mListenerMap.get(project);
+
+ if (listeners != null) {
+ for (ILayoutReloadListener listener : listeners) {
+ try {
+ listener.reloadLayout(flags, libraryChanged);
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Failed to call ILayoutReloadListener.reloadLayout");
+ }
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Implementation of the {@link IResourceListener} as an internal class so that the methods
+ * do not appear in the public API of {@link LayoutReloadMonitor}.
+ */
+ private IResourceListener mResourceListener = new IResourceListener() {
+
+ @Override
+ public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
+ // if this project has already been marked as modified, we do nothing.
+ ChangeFlags changeFlags = mProjectFlags.get(project);
+ if (changeFlags != null && changeFlags.isAllTrue()) {
+ return;
+ }
+
+ // this means a new resource folder was added or removed, which can impact the
+ // locale list.
+ if (changeFlags == null) {
+ changeFlags = new ChangeFlags();
+ mProjectFlags.put(project, changeFlags);
+ }
+
+ changeFlags.localeList = true;
+ }
+
+ @Override
+ public void fileChanged(IProject project, ResourceFile file, int eventType) {
+ // if this project has already been marked as modified, we do nothing.
+ ChangeFlags changeFlags = mProjectFlags.get(project);
+ if (changeFlags != null && changeFlags.isAllTrue()) {
+ return;
+ }
+
+ // now check that the file is *NOT* a layout file (those automatically trigger a layout
+ // reload and we don't want to do it twice.)
+ Collection<ResourceType> resTypes = file.getResourceTypes();
+
+ // it's unclear why but there has been cases of resTypes being empty!
+ if (resTypes.size() > 0) {
+ // this is a resource change, that may require a layout redraw!
+ if (changeFlags == null) {
+ changeFlags = new ChangeFlags();
+ mProjectFlags.put(project, changeFlags);
+ }
+
+ changeFlags.resources = true;
+ }
+ }
+ };
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java
new file mode 100644
index 000000000..020c666b9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
+import static com.android.SdkConstants.CALENDAR_VIEW;
+import static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_GRID_VIEW;
+import static com.android.SdkConstants.FQCN_SPINNER;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.LIST_VIEW;
+import static com.android.SdkConstants.SPINNER;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+
+import com.android.SdkConstants;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.api.ActionBarCallback;
+import com.android.ide.common.rendering.api.AdapterBinding;
+import com.android.ide.common.rendering.api.DataBindingItem;
+import com.android.ide.common.rendering.api.Features;
+import com.android.ide.common.rendering.api.ILayoutPullParser;
+import com.android.ide.common.rendering.api.IProjectCallback;
+import com.android.ide.common.rendering.api.LayoutlibCallback;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderLogger;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectClassLoader;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.resources.ResourceType;
+import com.android.util.Pair;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.eclipse.core.resources.IProject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Loader for Android Project class in order to use them in the layout editor.
+ * <p/>This implements {@link IProjectCallback} for the old and new API through
+ * {@link LayoutlibCallback}
+ */
+public final class ProjectCallback extends LayoutlibCallback {
+ private final HashMap<String, Class<?>> mLoadedClasses = new HashMap<String, Class<?>>();
+ private final Set<String> mMissingClasses = new TreeSet<String>();
+ private final Set<String> mBrokenClasses = new TreeSet<String>();
+ private final IProject mProject;
+ private final ClassLoader mParentClassLoader;
+ private final ProjectResources mProjectRes;
+ private final Object mCredential;
+ private boolean mUsed = false;
+ private String mNamespace;
+ private ProjectClassLoader mLoader = null;
+ private LayoutLog mLogger;
+ private LayoutLibrary mLayoutLib;
+ private String mLayoutName;
+ private ILayoutPullParser mLayoutEmbeddedParser;
+ private ResourceResolver mResourceResolver;
+ private GraphicalEditorPart mEditor;
+
+ /**
+ * Creates a new {@link ProjectCallback} to be used with the layout lib.
+ *
+ * @param layoutLib The layout library this callback is going to be invoked from
+ * @param projectRes the {@link ProjectResources} for the project.
+ * @param project the project.
+ * @param credential the sandbox credential
+ */
+ public ProjectCallback(LayoutLibrary layoutLib,
+ ProjectResources projectRes, IProject project, Object credential,
+ GraphicalEditorPart editor) {
+ mLayoutLib = layoutLib;
+ mParentClassLoader = layoutLib.getClassLoader();
+ mProjectRes = projectRes;
+ mProject = project;
+ mCredential = credential;
+ mEditor = editor;
+ }
+
+ public Set<String> getMissingClasses() {
+ return mMissingClasses;
+ }
+
+ public Set<String> getUninstantiatableClasses() {
+ return mBrokenClasses;
+ }
+
+ /**
+ * Sets the {@link LayoutLog} logger to use for error messages during problems
+ *
+ * @param logger the new logger to use, or null to clear it out
+ */
+ public void setLogger(LayoutLog logger) {
+ mLogger = logger;
+ }
+
+ /**
+ * Returns the {@link LayoutLog} logger used for error messages, or null
+ *
+ * @return the logger being used, or null if no logger is in use
+ */
+ public LayoutLog getLogger() {
+ return mLogger;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation goes through the output directory of the Eclipse project and loads the
+ * <code>.class</code> file directly.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object loadView(String className, Class[] constructorSignature,
+ Object[] constructorParameters)
+ throws Exception {
+ mUsed = true;
+
+ if (className == null) {
+ // Just make a plain <View> if you specify <view> without a class= attribute.
+ className = CLASS_VIEW;
+ }
+
+ // look for a cached version
+ Class<?> clazz = mLoadedClasses.get(className);
+ if (clazz != null) {
+ return instantiateClass(clazz, constructorSignature, constructorParameters);
+ }
+
+ // load the class.
+
+ try {
+ if (mLoader == null) {
+ // Allow creating class loaders during rendering; may be prevented by the
+ // RenderSecurityManager
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ mLoader = new ProjectClassLoader(mParentClassLoader, mProject);
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+ clazz = mLoader.loadClass(className);
+ } catch (Exception e) {
+ // Add the missing class to the list so that the renderer can print them later.
+ // no need to log this.
+ if (!className.equals(VIEW_FRAGMENT) && !className.equals(VIEW_INCLUDE)) {
+ mMissingClasses.add(className);
+ }
+ }
+
+ try {
+ if (clazz != null) {
+ // first try to instantiate it because adding it the list of loaded class so that
+ // we don't add broken classes.
+ Object view = instantiateClass(clazz, constructorSignature, constructorParameters);
+ mLoadedClasses.put(className, clazz);
+
+ return view;
+ }
+ } catch (Throwable e) {
+ // Find root cause to log it.
+ while (e.getCause() != null) {
+ e = e.getCause();
+ }
+
+ appendToIdeLog(e, "%1$s failed to instantiate.", className); //$NON-NLS-1$
+
+ // Add the missing class to the list so that the renderer can print them later.
+ if (mLogger instanceof RenderLogger) {
+ RenderLogger renderLogger = (RenderLogger) mLogger;
+ renderLogger.recordThrowable(e);
+
+ }
+ mBrokenClasses.add(className);
+ }
+
+ // Create a mock view instead. We don't cache it in the mLoadedClasses map.
+ // If any exception is thrown, we'll return a CFN with the original class name instead.
+ try {
+ clazz = mLoader.loadClass(SdkConstants.CLASS_MOCK_VIEW);
+ Object view = instantiateClass(clazz, constructorSignature, constructorParameters);
+
+ // Set the text of the mock view to the simplified name of the custom class
+ Method m = view.getClass().getMethod("setText",
+ new Class<?>[] { CharSequence.class });
+ String label = getShortClassName(className);
+ if (label.equals(VIEW_FRAGMENT)) {
+ label = "<fragment>\n"
+ + "Pick preview layout from the \"Fragment Layout\" context menu";
+ } else if (label.equals(VIEW_INCLUDE)) {
+ label = "Text";
+ }
+
+ m.invoke(view, label);
+
+ // Call MockView.setGravity(Gravity.CENTER) to get the text centered in
+ // MockViews.
+ // TODO: Do this in layoutlib's MockView class instead.
+ try {
+ // Look up android.view.Gravity#CENTER - or can we just hard-code
+ // the value (17) here?
+ Class<?> gravity =
+ Class.forName("android.view.Gravity", //$NON-NLS-1$
+ true, view.getClass().getClassLoader());
+ Field centerField = gravity.getField("CENTER"); //$NON-NLS-1$
+ int center = centerField.getInt(null);
+ m = view.getClass().getMethod("setGravity",
+ new Class<?>[] { Integer.TYPE });
+ // Center
+ //int center = (0x0001 << 4) | (0x0001 << 0);
+ m.invoke(view, Integer.valueOf(center));
+ } catch (Exception e) {
+ // Not important to center views
+ }
+
+ return view;
+ } catch (Exception e) {
+ // We failed to create and return a mock view.
+ // Just throw back a CNF with the original class name.
+ throw new ClassNotFoundException(className, e);
+ }
+ }
+
+ private String getShortClassName(String fqcn) {
+ // The name is typically a fully-qualified class name. Let's make it a tad shorter.
+
+ if (fqcn.startsWith("android.")) { //$NON-NLS-1$
+ // For android classes, convert android.foo.Name to android...Name
+ int first = fqcn.indexOf('.');
+ int last = fqcn.lastIndexOf('.');
+ if (last > first) {
+ return fqcn.substring(0, first) + ".." + fqcn.substring(last); //$NON-NLS-1$
+ }
+ } else {
+ // For custom non-android classes, it's best to keep the 2 first segments of
+ // the namespace, e.g. we want to get something like com.example...MyClass
+ int first = fqcn.indexOf('.');
+ first = fqcn.indexOf('.', first + 1);
+ int last = fqcn.lastIndexOf('.');
+ if (last > first) {
+ return fqcn.substring(0, first) + ".." + fqcn.substring(last); //$NON-NLS-1$
+ }
+ }
+
+ return fqcn;
+ }
+
+ /**
+ * Returns the namespace for the project. The namespace contains a standard part + the
+ * application package.
+ *
+ * @return The package namespace of the project or null in case of error.
+ */
+ @Override
+ public String getNamespace() {
+ if (mNamespace == null) {
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ ManifestData manifestData = AndroidManifestHelper.parseForData(mProject);
+ if (manifestData != null) {
+ String javaPackage = manifestData.getPackage();
+ mNamespace = String.format(AdtConstants.NS_CUSTOM_RESOURCES, javaPackage);
+ }
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+
+ return mNamespace;
+ }
+
+ @Override
+ public Pair<ResourceType, String> resolveResourceId(int id) {
+ if (mProjectRes != null) {
+ return mProjectRes.resolveResourceId(id);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String resolveResourceId(int[] id) {
+ if (mProjectRes != null) {
+ return mProjectRes.resolveStyleable(id);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Integer getResourceId(ResourceType type, String name) {
+ if (mProjectRes != null) {
+ return mProjectRes.getResourceId(type, name);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the loader has received requests to load custom views. Note that
+ * the custom view loading may not actually have succeeded; this flag only records
+ * whether it was <b>requested</b>.
+ * <p/>
+ * This allows to efficiently only recreate when needed upon code change in the
+ * project.
+ *
+ * @return true if the loader has been asked to load custom views
+ */
+ public boolean isUsed() {
+ return mUsed;
+ }
+
+ /**
+ * Instantiate a class object, using a specific constructor and parameters.
+ * @param clazz the class to instantiate
+ * @param constructorSignature the signature of the constructor to use
+ * @param constructorParameters the parameters to use in the constructor.
+ * @return A new class object, created using a specific constructor and parameters.
+ * @throws Exception
+ */
+ @SuppressWarnings("unchecked")
+ private Object instantiateClass(Class<?> clazz,
+ Class[] constructorSignature,
+ Object[] constructorParameters) throws Exception {
+ Constructor<?> constructor = null;
+
+ try {
+ constructor = clazz.getConstructor(constructorSignature);
+
+ } catch (NoSuchMethodException e) {
+ // Custom views can either implement a 3-parameter, 2-parameter or a
+ // 1-parameter. Let's synthetically build and try all the alternatives.
+ // That's kind of like switching to the other box.
+ //
+ // The 3-parameter constructor takes the following arguments:
+ // ...(Context context, AttributeSet attrs, int defStyle)
+
+ int n = constructorSignature.length;
+ if (n == 0) {
+ // There is no parameter-less constructor. Nobody should ask for one.
+ throw e;
+ }
+
+ for (int i = 3; i >= 1; i--) {
+ if (i == n) {
+ // Let's skip the one we know already fails
+ continue;
+ }
+ Class[] sig = new Class[i];
+ Object[] params = new Object[i];
+
+ int k = i;
+ if (n < k) {
+ k = n;
+ }
+ System.arraycopy(constructorSignature, 0, sig, 0, k);
+ System.arraycopy(constructorParameters, 0, params, 0, k);
+
+ for (k++; k <= i; k++) {
+ if (k == 2) {
+ // Parameter 2 is the AttributeSet
+ sig[k-1] = clazz.getClassLoader().loadClass("android.util.AttributeSet");
+ params[k-1] = null;
+
+ } else if (k == 3) {
+ // Parameter 3 is the int defstyle
+ sig[k-1] = int.class;
+ params[k-1] = 0;
+ }
+ }
+
+ constructorSignature = sig;
+ constructorParameters = params;
+
+ try {
+ // Try again...
+ constructor = clazz.getConstructor(constructorSignature);
+ if (constructor != null) {
+ // Found a suitable constructor, now let's use it.
+ // (But let's warn the user if the simple View constructor was found
+ // since Unexpected Things may happen if the attribute set constructors
+ // are not found)
+ if (constructorSignature.length < 2 && mLogger != null) {
+ mLogger.warning("wrongconstructor", //$NON-NLS-1$
+ String.format("Custom view %1$s is not using the 2- or 3-argument "
+ + "View constructors; XML attributes will not work",
+ clazz.getSimpleName()), null /*data*/);
+ }
+ break;
+ }
+ } catch (NoSuchMethodException e1) {
+ // pass
+ }
+ }
+
+ // If all the alternatives failed, throw the initial exception.
+ if (constructor == null) {
+ throw e;
+ }
+ }
+
+ constructor.setAccessible(true);
+ return constructor.newInstance(constructorParameters);
+ }
+
+ public void setLayoutParser(String layoutName, ILayoutPullParser layoutParser) {
+ mLayoutName = layoutName;
+ mLayoutEmbeddedParser = layoutParser;
+ }
+
+ @Override
+ public ILayoutPullParser getParser(String layoutName) {
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ // Try to compute the ResourceValue for this layout since layoutlib
+ // must be an older version which doesn't pass the value:
+ if (mResourceResolver != null) {
+ ResourceValue value = mResourceResolver.getProjectResource(ResourceType.LAYOUT,
+ layoutName);
+ if (value != null) {
+ return getParser(value);
+ }
+ }
+
+ return getParser(layoutName, null);
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+
+ @Override
+ public ILayoutPullParser getParser(ResourceValue layoutResource) {
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ return getParser(layoutResource.getName(),
+ new File(layoutResource.getValue()));
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+
+ private ILayoutPullParser getParser(String layoutName, File xml) {
+ if (layoutName.equals(mLayoutName)) {
+ ILayoutPullParser parser = mLayoutEmbeddedParser;
+ // The parser should only be used once!! If it is included more than once,
+ // subsequent includes should just use a plain pull parser that is not tied
+ // to the XML model
+ mLayoutEmbeddedParser = null;
+ return parser;
+ }
+
+ // For included layouts, create a ContextPullParser such that we get the
+ // layout editor behavior in included layouts as well - which for example
+ // replaces <fragment> tags with <include>.
+ if (xml != null && xml.isFile()) {
+ ContextPullParser parser = new ContextPullParser(this, xml);
+ try {
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ String xmlText = Files.toString(xml, Charsets.UTF_8);
+ parser.setInput(new StringReader(xmlText));
+ return parser;
+ } catch (XmlPullParserException e) {
+ appendToIdeLog(e, null);
+ } catch (FileNotFoundException e) {
+ // Shouldn't happen since we check isFile() above
+ } catch (IOException e) {
+ appendToIdeLog(e, null);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public Object getAdapterItemValue(ResourceReference adapterView, Object adapterCookie,
+ ResourceReference itemRef,
+ int fullPosition, int typePosition, int fullChildPosition, int typeChildPosition,
+ ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue) {
+
+ // Special case for the palette preview
+ if (viewAttribute == ViewAttribute.TEXT
+ && adapterView.getName().startsWith("android_widget_")) { //$NON-NLS-1$
+ String name = adapterView.getName();
+ if (viewRef.getName().equals("text2")) { //$NON-NLS-1$
+ return "Sub Item";
+ }
+ if (fullPosition == 0) {
+ String viewName = name.substring("android_widget_".length());
+ if (viewName.equals(EXPANDABLE_LIST_VIEW)) {
+ return "ExpandableList"; // ExpandableListView is too wide, character-wraps
+ }
+ return viewName;
+ } else {
+ return "Next Item";
+ }
+ }
+
+ if (itemRef.isFramework()) {
+ // Special case for list_view_item_2 and friends
+ if (viewRef.getName().equals("text2")) { //$NON-NLS-1$
+ return "Sub Item " + (fullPosition + 1);
+ }
+ }
+
+ if (viewAttribute == ViewAttribute.TEXT && ((String) defaultValue).length() == 0) {
+ return "Item " + (fullPosition + 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * For the given class, finds and returns the nearest super class which is a ListView
+ * or an ExpandableListView or a GridView (which uses a list adapter), or returns null.
+ *
+ * @param clz the class of the view object
+ * @return the fully qualified class name of the list ancestor, or null if there
+ * is no list view ancestor
+ */
+ public static String getListAdapterViewFqcn(Class<?> clz) {
+ String fqcn = clz.getName();
+ if (fqcn.endsWith(LIST_VIEW)) { // including EXPANDABLE_LIST_VIEW
+ return fqcn;
+ } else if (fqcn.equals(FQCN_GRID_VIEW)) {
+ return fqcn;
+ } else if (fqcn.equals(FQCN_SPINNER)) {
+ return fqcn;
+ } else if (fqcn.startsWith(ANDROID_PKG_PREFIX)) {
+ return null;
+ }
+ Class<?> superClass = clz.getSuperclass();
+ if (superClass != null) {
+ return getListAdapterViewFqcn(superClass);
+ } else {
+ // Should not happen; we would have encountered android.view.View first,
+ // and it should have been covered by the ANDROID_PKG_PREFIX case above.
+ return null;
+ }
+ }
+
+ /**
+ * Looks at the parent-chain of the view and if it finds a custom view, or a
+ * CalendarView, within the given distance then it returns true. A ListView within a
+ * CalendarView should not be assigned a custom list view type because it sets its own
+ * and then attempts to cast the layout to its own type which would fail if the normal
+ * default list item binding is used.
+ */
+ private boolean isWithinIllegalParent(Object viewObject, int depth) {
+ String fqcn = viewObject.getClass().getName();
+ if (fqcn.endsWith(CALENDAR_VIEW) || !fqcn.startsWith(ANDROID_PKG_PREFIX)) {
+ return true;
+ }
+
+ if (depth > 0) {
+ Result result = mLayoutLib.getViewParent(viewObject);
+ if (result.isSuccess()) {
+ Object parent = result.getData();
+ if (parent != null) {
+ return isWithinIllegalParent(parent, depth -1);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public AdapterBinding getAdapterBinding(final ResourceReference adapterView,
+ final Object adapterCookie, final Object viewObject) {
+ // Look for user-recorded preference for layout to be used for previews
+ if (adapterCookie instanceof UiViewElementNode) {
+ UiViewElementNode uiNode = (UiViewElementNode) adapterCookie;
+ AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, uiNode);
+ if (binding != null) {
+ return binding;
+ }
+ } else if (adapterCookie instanceof Map<?,?>) {
+ @SuppressWarnings("unchecked")
+ Map<String, String> map = (Map<String, String>) adapterCookie;
+ AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, map);
+ if (binding != null) {
+ return binding;
+ }
+ }
+
+ if (viewObject == null) {
+ return null;
+ }
+
+ // Is this a ListView or ExpandableListView? If so, return its fully qualified
+ // class name, otherwise return null. This is used to filter out other types
+ // of AdapterViews (such as Spinners) where we don't want to use the list item
+ // binding.
+ String listFqcn = getListAdapterViewFqcn(viewObject.getClass());
+ if (listFqcn == null) {
+ return null;
+ }
+
+ // Is this ListView nested within an "illegal" container, such as a CalendarView?
+ // If so, don't change the bindings below. Some views, such as CalendarView, and
+ // potentially some custom views, might be doing specific things with the ListView
+ // that could break if we add our own list binding, so for these leave the list
+ // alone.
+ if (isWithinIllegalParent(viewObject, 2)) {
+ return null;
+ }
+
+ int count = listFqcn.endsWith(GRID_VIEW) ? 24 : 12;
+ AdapterBinding binding = new AdapterBinding(count);
+ if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
+ binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_EXPANDABLE_LIST_ITEM,
+ true /* isFramework */, 1));
+ } else if (listFqcn.equals(SPINNER)) {
+ binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_SPINNER_ITEM,
+ true /* isFramework */, 1));
+ } else {
+ binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_LIST_ITEM,
+ true /* isFramework */, 1));
+ }
+
+ return binding;
+ }
+
+ /**
+ * Sets the {@link ResourceResolver} to be used when looking up resources
+ *
+ * @param resolver the resolver to use
+ */
+ public void setResourceResolver(ResourceResolver resolver) {
+ mResourceResolver = resolver;
+ }
+
+ // Append the given message to the ADT log. Bypass the sandbox if necessary
+ // such that we can write to the log file.
+ private void appendToIdeLog(Throwable exception, String format, Object ... args) {
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ AdtPlugin.log(exception, format, args);
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+
+ @Override
+ public ActionBarCallback getActionBarCallback() {
+ return new ActionBarHandler(mEditor);
+ }
+
+ @Override
+ public boolean supports(int feature) {
+ return feature <= Features.LAST_CAPABILITY;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java
new file mode 100644
index 000000000..858156884
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_PADDING;
+import static com.android.SdkConstants.AUTO_URI;
+import static com.android.SdkConstants.UNIT_DIP;
+import static com.android.SdkConstants.UNIT_DP;
+import static com.android.SdkConstants.UNIT_IN;
+import static com.android.SdkConstants.UNIT_MM;
+import static com.android.SdkConstants.UNIT_PT;
+import static com.android.SdkConstants.UNIT_PX;
+import static com.android.SdkConstants.UNIT_SP;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+
+import com.android.ide.common.rendering.api.ILayoutPullParser;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.res2.ValueXmlHelper;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.FragmentMenu;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.Density;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@link ILayoutPullParser} implementation on top of {@link UiElementNode}.
+ * <p/>
+ * It's designed to work on layout files, and will most likely not work on other resource files.
+ * <p/>
+ * This pull parser generates {@link ViewInfo}s which key is a {@link UiElementNode}.
+ */
+public class UiElementPullParser extends BasePullParser {
+ private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$
+
+ private final int[] sIntOut = new int[1];
+
+ private final ArrayList<UiElementNode> mNodeStack = new ArrayList<UiElementNode>();
+ private UiElementNode mRoot;
+ private final boolean mExplodedRendering;
+ private boolean mZeroAttributeIsPadding = false;
+ private boolean mIncreaseExistingPadding = false;
+ private LayoutDescriptors mDescriptors;
+ private final Density mDensity;
+
+ /**
+ * Number of pixels to pad views with in exploded-rendering mode.
+ */
+ private static final String DEFAULT_PADDING_VALUE =
+ ExplodedRenderingHelper.PADDING_VALUE + UNIT_PX;
+
+ /**
+ * Number of pixels to pad exploded individual views with. (This is HALF the width of the
+ * rectangle since padding is repeated on both sides of the empty content.)
+ */
+ private static final String FIXED_PADDING_VALUE = "20px"; //$NON-NLS-1$
+
+ /**
+ * Set of nodes that we want to auto-pad using {@link #FIXED_PADDING_VALUE} as the padding
+ * attribute value. Can be null, which is the case when we don't want to perform any
+ * <b>individual</b> node exploding.
+ */
+ private final Set<UiElementNode> mExplodeNodes;
+
+ /**
+ * Constructs a new {@link UiElementPullParser}, a parser dedicated to the special case of
+ * parsing a layout resource files, and handling "exploded rendering" - adding padding on views
+ * to make them easier to see and operate on.
+ *
+ * @param top The {@link UiElementNode} for the root node.
+ * @param explodeRendering When true, add padding to <b>all</b> nodes in the hierarchy. This
+ * will add rather than replace padding of a node.
+ * @param explodeNodes A set of individual nodes that should be assigned a fixed amount of
+ * padding ({@link #FIXED_PADDING_VALUE}). This is intended for use with nodes that
+ * (without padding) would be invisible. This parameter can be null, in which case
+ * nodes are not individually exploded (but they may all be exploded with the
+ * explodeRendering parameter.
+ * @param density the density factor for the screen.
+ * @param project Project containing this layout.
+ */
+ public UiElementPullParser(UiElementNode top, boolean explodeRendering,
+ Set<UiElementNode> explodeNodes,
+ Density density, IProject project) {
+ super();
+ mRoot = top;
+ mExplodedRendering = explodeRendering;
+ mExplodeNodes = explodeNodes;
+ mDensity = density;
+ if (mExplodedRendering) {
+ // get the layout descriptor
+ IAndroidTarget target = Sdk.getCurrent().getTarget(project);
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ mDescriptors = data.getLayoutDescriptors();
+ }
+ push(mRoot);
+ }
+
+ protected UiElementNode getCurrentNode() {
+ if (mNodeStack.size() > 0) {
+ return mNodeStack.get(mNodeStack.size()-1);
+ }
+
+ return null;
+ }
+
+ private Node getAttribute(int i) {
+ if (mParsingState != START_TAG) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ // get the current uiNode
+ UiElementNode uiNode = getCurrentNode();
+
+ // get its xml node
+ Node xmlNode = uiNode.getXmlNode();
+
+ if (xmlNode != null) {
+ return xmlNode.getAttributes().item(i);
+ }
+
+ return null;
+ }
+
+ private void push(UiElementNode node) {
+ mNodeStack.add(node);
+
+ mZeroAttributeIsPadding = false;
+ mIncreaseExistingPadding = false;
+
+ if (mExplodedRendering) {
+ // first get the node name
+ String xml = node.getDescriptor().getXmlLocalName();
+ ViewElementDescriptor descriptor = mDescriptors.findDescriptorByTag(xml);
+ if (descriptor != null) {
+ NamedNodeMap attributes = node.getXmlNode().getAttributes();
+ Node padding = attributes.getNamedItemNS(ANDROID_URI, ATTR_PADDING);
+ if (padding == null) {
+ // we'll return an extra padding
+ mZeroAttributeIsPadding = true;
+ } else {
+ mIncreaseExistingPadding = true;
+ }
+ }
+ }
+ }
+
+ private UiElementNode pop() {
+ return mNodeStack.remove(mNodeStack.size()-1);
+ }
+
+ // ------------- IXmlPullParser --------
+
+ /**
+ * {@inheritDoc}
+ * <p/>
+ * This implementation returns the underlying DOM node of type {@link UiElementNode}.
+ * Note that the link between the GLE and the parsing code depends on this being the actual
+ * type returned, so you can't just randomly change it here.
+ * <p/>
+ * Currently used by:
+ * - private method GraphicalLayoutEditor#updateNodeWithBounds(ILayoutViewInfo).
+ * - private constructor of LayoutCanvas.CanvasViewInfo.
+ */
+ @Override
+ public Object getViewCookie() {
+ return getCurrentNode();
+ }
+
+ /**
+ * Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser}
+ */
+ @Override
+ public Object getViewKey() {
+ return getViewCookie();
+ }
+
+ /**
+ * This implementation does nothing for now as all the embedded XML will use a normal KXML
+ * parser.
+ */
+ @Override
+ public ILayoutPullParser getParser(String layoutName) {
+ return null;
+ }
+
+ // ------------- XmlPullParser --------
+
+ @Override
+ public String getPositionDescription() {
+ return "XML DOM element depth:" + mNodeStack.size();
+ }
+
+ /*
+ * This does not seem to be called by the layoutlib, but we keep this (and maintain
+ * it) just in case.
+ */
+ @Override
+ public int getAttributeCount() {
+ UiElementNode node = getCurrentNode();
+
+ if (node != null) {
+ Collection<UiAttributeNode> attributes = node.getAllUiAttributes();
+ int count = attributes.size();
+
+ return count + (mZeroAttributeIsPadding ? 1 : 0);
+ }
+
+ return 0;
+ }
+
+ /*
+ * This does not seem to be called by the layoutlib, but we keep this (and maintain
+ * it) just in case.
+ */
+ @Override
+ public String getAttributeName(int i) {
+ if (mZeroAttributeIsPadding) {
+ if (i == 0) {
+ return ATTR_PADDING;
+ } else {
+ i--;
+ }
+ }
+
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getLocalName();
+ }
+
+ return null;
+ }
+
+ /*
+ * This does not seem to be called by the layoutlib, but we keep this (and maintain
+ * it) just in case.
+ */
+ @Override
+ public String getAttributeNamespace(int i) {
+ if (mZeroAttributeIsPadding) {
+ if (i == 0) {
+ return ANDROID_URI;
+ } else {
+ i--;
+ }
+ }
+
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getNamespaceURI();
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ /*
+ * This does not seem to be called by the layoutlib, but we keep this (and maintain
+ * it) just in case.
+ */
+ @Override
+ public String getAttributePrefix(int i) {
+ if (mZeroAttributeIsPadding) {
+ if (i == 0) {
+ // figure out the prefix associated with the android namespace.
+ Document doc = mRoot.getXmlDocument();
+ return doc.lookupPrefix(ANDROID_URI);
+ } else {
+ i--;
+ }
+ }
+
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getPrefix();
+ }
+ return null;
+ }
+
+ /*
+ * This does not seem to be called by the layoutlib, but we keep this (and maintain
+ * it) just in case.
+ */
+ @Override
+ public String getAttributeValue(int i) {
+ if (mZeroAttributeIsPadding) {
+ if (i == 0) {
+ return DEFAULT_PADDING_VALUE;
+ } else {
+ i--;
+ }
+ }
+
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ String value = attribute.getNodeValue();
+ if (mIncreaseExistingPadding && ATTR_PADDING.equals(attribute.getLocalName()) &&
+ ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // add the padding and return the value
+ return addPaddingToValue(value);
+ }
+ return value;
+ }
+
+ return null;
+ }
+
+ /*
+ * This is the main method used by the LayoutInflater to query for attributes.
+ */
+ @Override
+ public String getAttributeValue(String namespace, String localName) {
+ if (mExplodeNodes != null && ATTR_PADDING.equals(localName) &&
+ ANDROID_URI.equals(namespace)) {
+ UiElementNode node = getCurrentNode();
+ if (node != null && mExplodeNodes.contains(node)) {
+ return FIXED_PADDING_VALUE;
+ }
+ }
+
+ if (mZeroAttributeIsPadding && ATTR_PADDING.equals(localName) &&
+ ANDROID_URI.equals(namespace)) {
+ return DEFAULT_PADDING_VALUE;
+ }
+
+ // get the current uiNode
+ UiElementNode uiNode = getCurrentNode();
+
+ // get its xml node
+ Node xmlNode = uiNode.getXmlNode();
+
+ if (xmlNode != null) {
+ if (ATTR_LAYOUT.equals(localName) && VIEW_FRAGMENT.equals(xmlNode.getNodeName())) {
+ String layout = FragmentMenu.getFragmentLayout(xmlNode);
+ if (layout != null) {
+ return layout;
+ }
+ }
+
+ Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName);
+
+ // Auto-convert http://schemas.android.com/apk/res-auto resources. The lookup
+ // will be for the current application's resource package, e.g.
+ // http://schemas.android.com/apk/res/foo.bar, but the XML document will
+ // be using http://schemas.android.com/apk/res-auto in library projects:
+ if (attribute == null && namespace != null && !namespace.equals(ANDROID_URI)) {
+ attribute = xmlNode.getAttributes().getNamedItemNS(AUTO_URI, localName);
+ }
+
+ if (attribute != null) {
+ String value = attribute.getNodeValue();
+ if (mIncreaseExistingPadding && ATTR_PADDING.equals(localName) &&
+ ANDROID_URI.equals(namespace)) {
+ // add the padding and return the value
+ return addPaddingToValue(value);
+ }
+
+ // on the fly convert match_parent to fill_parent for compatibility with older
+ // platforms.
+ if (VALUE_MATCH_PARENT.equals(value) &&
+ (ATTR_LAYOUT_WIDTH.equals(localName) ||
+ ATTR_LAYOUT_HEIGHT.equals(localName)) &&
+ ANDROID_URI.equals(namespace)) {
+ return VALUE_FILL_PARENT;
+ }
+
+ // Handle unicode escapes etc
+ value = ValueXmlHelper.unescapeResourceString(value, false, false);
+
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getDepth() {
+ return mNodeStack.size();
+ }
+
+ @Override
+ public String getName() {
+ if (mParsingState == START_TAG || mParsingState == END_TAG) {
+ String name = getCurrentNode().getDescriptor().getXmlLocalName();
+
+ if (name.equals(VIEW_FRAGMENT)) {
+ // Temporarily translate <fragment> to <include> (and in getAttribute
+ // we will also provide a layout-attribute for the corresponding
+ // fragment name attribute)
+ String layout = FragmentMenu.getFragmentLayout(getCurrentNode().getXmlNode());
+ if (layout != null) {
+ return VIEW_INCLUDE;
+ }
+ }
+
+ return name;
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getNamespace() {
+ if (mParsingState == START_TAG || mParsingState == END_TAG) {
+ return getCurrentNode().getDescriptor().getNamespace();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getPrefix() {
+ if (mParsingState == START_TAG || mParsingState == END_TAG) {
+ Document doc = mRoot.getXmlDocument();
+ return doc.lookupPrefix(getCurrentNode().getDescriptor().getNamespace());
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isEmptyElementTag() throws XmlPullParserException {
+ if (mParsingState == START_TAG) {
+ return getCurrentNode().getUiChildren().size() == 0;
+ }
+
+ throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG",
+ this, null);
+ }
+
+ @Override
+ public void onNextFromStartDocument() {
+ onNextFromStartTag();
+ }
+
+ @Override
+ public void onNextFromStartTag() {
+ // get the current node, and look for text or children (children first)
+ UiElementNode node = getCurrentNode();
+ List<UiElementNode> children = node.getUiChildren();
+ if (children.size() > 0) {
+ // move to the new child, and don't change the state.
+ push(children.get(0));
+
+ // in case the current state is CURRENT_DOC, we set the proper state.
+ mParsingState = START_TAG;
+ } else {
+ if (mParsingState == START_DOCUMENT) {
+ // this handles the case where there's no node.
+ mParsingState = END_DOCUMENT;
+ } else {
+ mParsingState = END_TAG;
+ }
+ }
+ }
+
+ @Override
+ public void onNextFromEndTag() {
+ // look for a sibling. if no sibling, go back to the parent
+ UiElementNode node = getCurrentNode();
+ node = node.getUiNextSibling();
+ if (node != null) {
+ // to go to the sibling, we need to remove the current node,
+ pop();
+ // and add its sibling.
+ push(node);
+ mParsingState = START_TAG;
+ } else {
+ // move back to the parent
+ pop();
+
+ // we have only one element left (mRoot), then we're done with the document.
+ if (mNodeStack.size() == 1) {
+ mParsingState = END_DOCUMENT;
+ } else {
+ mParsingState = END_TAG;
+ }
+ }
+ }
+
+ // ------- TypedValue stuff
+ // This is adapted from com.android.layoutlib.bridge.ResourceHelper
+ // (but modified to directly take the parsed value and convert it into pixel instead of
+ // storing it into a TypedValue)
+ // this was originally taken from platform/frameworks/base/libs/utils/ResourceTypes.cpp
+
+ private static final class DimensionEntry {
+ String name;
+ int type;
+
+ DimensionEntry(String name, int unit) {
+ this.name = name;
+ this.type = unit;
+ }
+ }
+
+ /** {@link DimensionEntry} complex unit: Value is raw pixels. */
+ private static final int COMPLEX_UNIT_PX = 0;
+ /** {@link DimensionEntry} complex unit: Value is Device Independent
+ * Pixels. */
+ private static final int COMPLEX_UNIT_DIP = 1;
+ /** {@link DimensionEntry} complex unit: Value is a scaled pixel. */
+ private static final int COMPLEX_UNIT_SP = 2;
+ /** {@link DimensionEntry} complex unit: Value is in points. */
+ private static final int COMPLEX_UNIT_PT = 3;
+ /** {@link DimensionEntry} complex unit: Value is in inches. */
+ private static final int COMPLEX_UNIT_IN = 4;
+ /** {@link DimensionEntry} complex unit: Value is in millimeters. */
+ private static final int COMPLEX_UNIT_MM = 5;
+
+ private final static DimensionEntry[] sDimensions = new DimensionEntry[] {
+ new DimensionEntry(UNIT_PX, COMPLEX_UNIT_PX),
+ new DimensionEntry(UNIT_DIP, COMPLEX_UNIT_DIP),
+ new DimensionEntry(UNIT_DP, COMPLEX_UNIT_DIP),
+ new DimensionEntry(UNIT_SP, COMPLEX_UNIT_SP),
+ new DimensionEntry(UNIT_PT, COMPLEX_UNIT_PT),
+ new DimensionEntry(UNIT_IN, COMPLEX_UNIT_IN),
+ new DimensionEntry(UNIT_MM, COMPLEX_UNIT_MM),
+ };
+
+ /**
+ * Adds padding to an existing dimension.
+ * <p/>This will resolve the attribute value (which can be px, dip, dp, sp, pt, in, mm) to
+ * a pixel value, add the padding value ({@link ExplodedRenderingHelper#PADDING_VALUE}),
+ * and then return a string with the new value as a px string ("42px");
+ * If the conversion fails, only the special padding is returned.
+ */
+ private String addPaddingToValue(String s) {
+ int padding = ExplodedRenderingHelper.PADDING_VALUE;
+ if (stringToPixel(s)) {
+ padding += sIntOut[0];
+ }
+
+ return padding + UNIT_PX;
+ }
+
+ /**
+ * Convert the string into a pixel value, and puts it in {@link #sIntOut}
+ * @param s the dimension value from an XML attribute
+ * @return true if success.
+ */
+ private boolean stringToPixel(String s) {
+ // remove the space before and after
+ s = s.trim();
+ int len = s.length();
+
+ if (len <= 0) {
+ return false;
+ }
+
+ // check that there's no non ASCII characters.
+ char[] buf = s.toCharArray();
+ for (int i = 0 ; i < len ; i++) {
+ if (buf[i] > 255) {
+ return false;
+ }
+ }
+
+ // check the first character
+ if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.') {
+ return false;
+ }
+
+ // now look for the string that is after the float...
+ Matcher m = FLOAT_PATTERN.matcher(s);
+ if (m.matches()) {
+ String f_str = m.group(1);
+ String end = m.group(2);
+
+ float f;
+ try {
+ f = Float.parseFloat(f_str);
+ } catch (NumberFormatException e) {
+ // this shouldn't happen with the regexp above.
+ return false;
+ }
+
+ if (end.length() > 0 && end.charAt(0) != ' ') {
+ // We only support dimension-type values, so try to parse the unit for dimension
+ DimensionEntry dimension = parseDimension(end);
+ if (dimension != null) {
+ // convert the value into pixel based on the dimention type
+ // This is similar to TypedValue.applyDimension()
+ switch (dimension.type) {
+ case COMPLEX_UNIT_PX:
+ // do nothing, value is already in px
+ break;
+ case COMPLEX_UNIT_DIP:
+ case COMPLEX_UNIT_SP: // intended fall-through since we don't
+ // adjust for font size
+ f *= (float)mDensity.getDpiValue() / Density.DEFAULT_DENSITY;
+ break;
+ case COMPLEX_UNIT_PT:
+ f *= mDensity.getDpiValue() * (1.0f / 72);
+ break;
+ case COMPLEX_UNIT_IN:
+ f *= mDensity.getDpiValue();
+ break;
+ case COMPLEX_UNIT_MM:
+ f *= mDensity.getDpiValue() * (1.0f / 25.4f);
+ break;
+ }
+
+ // store result (converted to int)
+ sIntOut[0] = (int) (f + 0.5);
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static DimensionEntry parseDimension(String str) {
+ str = str.trim();
+
+ for (DimensionEntry d : sDimensions) {
+ if (d.name.equals(str)) {
+ return d;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/WidgetPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/WidgetPullParser.java
new file mode 100644
index 000000000..dce2ccbf1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/WidgetPullParser.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout;
+
+import com.android.SdkConstants;
+import com.android.ide.common.rendering.api.ILayoutPullParser;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * {@link ILayoutPullParser} implementation to render android widget bitmap.
+ * <p/>
+ * The parser emulates a layout that contains just one widget, described by the
+ * {@link ViewElementDescriptor} passed in the constructor.
+ * <p/>
+ * This pull parser generates {@link ILayoutViewInfo}s which key is a {@link ViewElementDescriptor}.
+ */
+public class WidgetPullParser extends BasePullParser {
+
+ private final ViewElementDescriptor mDescriptor;
+ private String[][] mAttributes = new String[][] {
+ { "text", null },
+ { "layout_width", "wrap_content" },
+ { "layout_height", "wrap_content" },
+ };
+
+ public WidgetPullParser(ViewElementDescriptor descriptor) {
+ mDescriptor = descriptor;
+
+ String[] segments = mDescriptor.getFullClassName().split(AdtConstants.RE_DOT);
+ mAttributes[0][1] = segments[segments.length-1];
+ }
+
+ @Override
+ public Object getViewCookie() {
+ // we need a viewKey or the ILayoutResult will not contain any ILayoutViewInfo
+ return mDescriptor;
+ }
+
+ /**
+ * Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser}
+ */
+ @Override
+ public Object getViewKey() {
+ return getViewCookie();
+ }
+
+ @Override
+ public ILayoutPullParser getParser(String layoutName) {
+ // there's no embedded layout for a single widget.
+ return null;
+ }
+
+ @Override
+ public int getAttributeCount() {
+ return mAttributes.length; // text attribute
+ }
+
+ @Override
+ public String getAttributeName(int index) {
+ if (index < mAttributes.length) {
+ return mAttributes[index][0];
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getAttributeNamespace(int index) {
+ return SdkConstants.NS_RESOURCES;
+ }
+
+ @Override
+ public String getAttributePrefix(int index) {
+ // pass
+ return null;
+ }
+
+ @Override
+ public String getAttributeValue(int index) {
+ if (index < mAttributes.length) {
+ return mAttributes[index][1];
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getAttributeValue(String ns, String name) {
+ if (SdkConstants.NS_RESOURCES.equals(ns)) {
+ for (String[] attribute : mAttributes) {
+ if (name.equals(attribute[0])) {
+ return attribute[1];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getDepth() {
+ // pass
+ return 0;
+ }
+
+ @Override
+ public String getName() {
+ return mDescriptor.getXmlLocalName();
+ }
+
+ @Override
+ public String getNamespace() {
+ // pass
+ return null;
+ }
+
+ @Override
+ public String getPositionDescription() {
+ // pass
+ return null;
+ }
+
+ @Override
+ public String getPrefix() {
+ // pass
+ return null;
+ }
+
+ @Override
+ public boolean isEmptyElementTag() throws XmlPullParserException {
+ if (mParsingState == START_TAG) {
+ return true;
+ }
+
+ throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG",
+ this, null);
+ }
+
+ @Override
+ public void onNextFromStartDocument() {
+ // just go to start_tag
+ mParsingState = START_TAG;
+ }
+
+ @Override
+ public void onNextFromStartTag() {
+ // since we have no children, just go to end_tag
+ mParsingState = END_TAG;
+ }
+
+ @Override
+ public void onNextFromEndTag() {
+ // just one tag. we are done.
+ mParsingState = END_DOCUMENT;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java
new file mode 100644
index 000000000..36cd0fbbb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jdt.ui.ISharedImages;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@linkplain ActivityMenuListener} class is responsible for
+ * generating the activity menu in the {@link ConfigurationChooser}.
+ */
+class ActivityMenuListener extends SelectionAdapter {
+ private static final int ACTION_OPEN_ACTIVITY = 1;
+ private static final int ACTION_SELECT_ACTIVITY = 2;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final int mAction;
+ private final String mFqcn;
+
+ ActivityMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ int action,
+ @Nullable String fqcn) {
+ mConfigChooser = configChooser;
+ mAction = action;
+ mFqcn = fqcn;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switch (mAction) {
+ case ACTION_OPEN_ACTIVITY: {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ String fqcn = configuration.getActivity();
+ AdtPlugin.openJavaClass(mConfigChooser.getProject(), fqcn);
+ break;
+ }
+ case ACTION_SELECT_ACTIVITY: {
+ mConfigChooser.selectActivity(mFqcn);
+ mConfigChooser.onSelectActivity();
+ break;
+ }
+ default: assert false : mAction;
+ }
+ }
+
+ static void show(ConfigurationChooser chooser, ToolItem combo) {
+ // TODO: Allow using fragments here as well?
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ ISharedImages sharedImages = JavaUI.getSharedImages();
+ Configuration configuration = chooser.getConfiguration();
+ String current = configuration.getActivity();
+
+ if (current != null) {
+ MenuItem item = new MenuItem(menu, SWT.PUSH);
+ String label = ConfigurationChooser.getActivityLabel(current, true);
+ item.setText( String.format("Open %1$s...", label));
+ Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CUNIT);
+ item.setImage(image);
+ item.addSelectionListener(
+ new ActivityMenuListener(chooser, ACTION_OPEN_ACTIVITY, null));
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ IProject project = chooser.getProject();
+ Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CLASS);
+
+ // Add activities found to be relevant to this layout
+ String layoutName = ResourceHelper.getLayoutName(chooser.getEditedFile());
+ String pkg = ManifestInfo.get(project).getPackage();
+ List<String> preferred = ManifestInfo.guessActivities(project, layoutName, pkg);
+ current = addActivities(chooser, menu, current, image, preferred);
+
+ // Add all activities
+ List<String> activities = ManifestInfo.getProjectActivities(project);
+ if (preferred.size() > 0) {
+ // Filter out the activities we've already listed above
+ List<String> filtered = new ArrayList<String>(activities.size());
+ Set<String> remove = new HashSet<String>(preferred);
+ for (String fqcn : activities) {
+ if (!remove.contains(fqcn)) {
+ filtered.add(fqcn);
+ }
+ }
+ activities = filtered;
+ }
+
+ if (activities.size() > 0) {
+ if (preferred.size() > 0) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ addActivities(chooser, menu, current, image, activities);
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ private static String addActivities(ConfigurationChooser chooser, Menu menu, String current,
+ Image image, List<String> activities) {
+ for (final String fqcn : activities) {
+ String title = ConfigurationChooser.getActivityLabel(fqcn, false);
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+ item.setImage(image);
+
+ boolean selected = title.equals(current);
+ if (selected) {
+ item.setSelection(true);
+ current = null; // Only show the first occurrence as selected
+ // such that we don't show it selected again in the full activity list
+ }
+
+ item.addSelectionListener(new ActivityMenuListener(chooser,
+ ACTION_SELECT_ACTIVITY, fqcn));
+ }
+
+ return current;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java
new file mode 100644
index 000000000..c4253cddf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.NightModeQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.common.resources.configuration.UiModeQualifier;
+import com.android.ide.common.resources.configuration.VersionQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.Density;
+import com.android.resources.LayoutDirection;
+import com.android.resources.NightMode;
+import com.android.resources.ScreenSize;
+import com.android.resources.UiMode;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.android.utils.Pair;
+import com.google.common.base.Objects;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.QualifiedName;
+
+import java.util.List;
+
+/**
+ * A {@linkplain Configuration} is a selection of device, orientation, theme,
+ * etc for use when rendering a layout.
+ */
+public class Configuration {
+ /** The {@link FolderConfiguration} in change flags or override flags */
+ public static final int CFG_FOLDER = 1 << 0;
+ /** The {@link Device} in change flags or override flags */
+ public static final int CFG_DEVICE = 1 << 1;
+ /** The {@link State} in change flags or override flags */
+ public static final int CFG_DEVICE_STATE = 1 << 2;
+ /** The theme in change flags or override flags */
+ public static final int CFG_THEME = 1 << 3;
+ /** The locale in change flags or override flags */
+ public static final int CFG_LOCALE = 1 << 4;
+ /** The rendering {@link IAndroidTarget} in change flags or override flags */
+ public static final int CFG_TARGET = 1 << 5;
+ /** The {@link NightMode} in change flags or override flags */
+ public static final int CFG_NIGHT_MODE = 1 << 6;
+ /** The {@link UiMode} in change flags or override flags */
+ public static final int CFG_UI_MODE = 1 << 7;
+ /** The {@link UiMode} in change flags or override flags */
+ public static final int CFG_ACTIVITY = 1 << 8;
+
+ /** References all attributes */
+ public static final int MASK_ALL = 0xFFFF;
+
+ /** Attributes which affect which best-layout-file selection */
+ public static final int MASK_FILE_ATTRS =
+ CFG_DEVICE|CFG_DEVICE_STATE|CFG_LOCALE|CFG_TARGET|CFG_NIGHT_MODE|CFG_UI_MODE;
+
+ /** Attributes which affect rendering appearance */
+ public static final int MASK_RENDERING = MASK_FILE_ATTRS|CFG_THEME;
+
+ /**
+ * Setting name for project-wide setting controlling rendering target and locale which
+ * is shared for all files
+ */
+ public final static QualifiedName NAME_RENDER_STATE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "render"); //$NON-NLS-1$
+
+ private final static String MARKER_FRAMEWORK = "-"; //$NON-NLS-1$
+ private final static String MARKER_PROJECT = "+"; //$NON-NLS-1$
+ private final static String SEP = ":"; //$NON-NLS-1$
+ private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
+
+ @NonNull
+ protected ConfigurationChooser mConfigChooser;
+
+ /** The {@link FolderConfiguration} representing the state of the UI controls */
+ @NonNull
+ protected final FolderConfiguration mFullConfig = new FolderConfiguration();
+
+ /** The {@link FolderConfiguration} being edited. */
+ @Nullable
+ protected FolderConfiguration mEditedConfig;
+
+ /** The target of the project of the file being edited. */
+ @Nullable
+ private IAndroidTarget mTarget;
+
+ /** The theme style to render with */
+ @Nullable
+ private String mTheme;
+
+ /** The device to render with */
+ @Nullable
+ private Device mDevice;
+
+ /** The device state */
+ @Nullable
+ private State mState;
+
+ /**
+ * The activity associated with the layout. This is just a cached value of
+ * the true value stored on the layout.
+ */
+ @Nullable
+ private String mActivity;
+
+ /** The locale to use for this configuration */
+ @NonNull
+ private Locale mLocale = Locale.ANY;
+
+ /** UI mode */
+ @NonNull
+ private UiMode mUiMode = UiMode.NORMAL;
+
+ /** Night mode */
+ @NonNull
+ private NightMode mNightMode = NightMode.NOTNIGHT;
+
+ /** The display name */
+ private String mDisplayName;
+
+ /**
+ * Creates a new {@linkplain Configuration}
+ *
+ * @param chooser the associated chooser
+ */
+ protected Configuration(@NonNull ConfigurationChooser chooser) {
+ mConfigChooser = chooser;
+ }
+
+ /**
+ * Sets the associated configuration chooser
+ *
+ * @param chooser the chooser
+ */
+ void setChooser(@NonNull ConfigurationChooser chooser) {
+ // TODO: We should get rid of the binding between configurations
+ // and configuration choosers. This is currently needed because
+ // the choosers contain vital data such as the set of available
+ // rendering targets, the set of available locales etc, which
+ // also doesn't belong inside the configuration but is needed by it.
+ mConfigChooser = chooser;
+ }
+
+ /**
+ * Gets the associated configuration chooser
+ *
+ * @return the chooser
+ */
+ @NonNull
+ ConfigurationChooser getChooser() {
+ return mConfigChooser;
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration}
+ *
+ * @param chooser the associated chooser
+ * @return a new configuration
+ */
+ @NonNull
+ public static Configuration create(@NonNull ConfigurationChooser chooser) {
+ return new Configuration(chooser);
+ }
+
+ /**
+ * Creates a configuration suitable for the given file
+ *
+ * @param base the base configuration to base the file configuration off of
+ * @param file the file to look up a configuration for
+ * @return a suitable configuration
+ */
+ @NonNull
+ public static Configuration create(
+ @NonNull Configuration base,
+ @NonNull IFile file) {
+ Configuration configuration = copy(base);
+ ConfigurationChooser chooser = base.getChooser();
+ ProjectResources resources = chooser.getResources();
+ ConfigurationMatcher matcher = new ConfigurationMatcher(chooser, configuration, file,
+ resources, false);
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ configuration.mEditedConfig = new FolderConfiguration();
+ configuration.mEditedConfig.set(resFolder.getConfiguration());
+
+ matcher.adaptConfigSelection(true /*needBestMatch*/);
+ configuration.syncFolderConfig();
+
+ return configuration;
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration} that is a copy from a different configuration
+ *
+ * @param original the original to copy from
+ * @return a new configuration copied from the original
+ */
+ @NonNull
+ public static Configuration copy(@NonNull Configuration original) {
+ Configuration copy = create(original.mConfigChooser);
+ copy.mFullConfig.set(original.mFullConfig);
+ if (original.mEditedConfig != null) {
+ copy.mEditedConfig = new FolderConfiguration();
+ copy.mEditedConfig.set(original.mEditedConfig);
+ }
+ copy.mTarget = original.getTarget();
+ copy.mTheme = original.getTheme();
+ copy.mDevice = original.getDevice();
+ copy.mState = original.getDeviceState();
+ copy.mActivity = original.getActivity();
+ copy.mLocale = original.getLocale();
+ copy.mUiMode = original.getUiMode();
+ copy.mNightMode = original.getNightMode();
+ copy.mDisplayName = original.getDisplayName();
+
+ return copy;
+ }
+
+ /**
+ * Returns the associated activity
+ *
+ * @return the activity
+ */
+ @Nullable
+ public String getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Returns the chosen device.
+ *
+ * @return the chosen device
+ */
+ @Nullable
+ public Device getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns the chosen device state
+ *
+ * @return the device state
+ */
+ @Nullable
+ public State getDeviceState() {
+ return mState;
+ }
+
+ /**
+ * Returns the chosen locale
+ *
+ * @return the locale
+ */
+ @NonNull
+ public Locale getLocale() {
+ return mLocale;
+ }
+
+ /**
+ * Returns the UI mode
+ *
+ * @return the UI mode
+ */
+ @NonNull
+ public UiMode getUiMode() {
+ return mUiMode;
+ }
+
+ /**
+ * Returns the day/night mode
+ *
+ * @return the night mode
+ */
+ @NonNull
+ public NightMode getNightMode() {
+ return mNightMode;
+ }
+
+ /**
+ * Returns the current theme style
+ *
+ * @return the theme style
+ */
+ @Nullable
+ public String getTheme() {
+ return mTheme;
+ }
+
+ /**
+ * Returns the rendering target
+ *
+ * @return the target
+ */
+ @Nullable
+ public IAndroidTarget getTarget() {
+ return mTarget;
+ }
+
+ /**
+ * Returns the display name to show for this configuration
+ *
+ * @return the display name, or null if none has been assigned
+ */
+ @Nullable
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Returns whether the configuration's theme is a project theme.
+ * <p/>
+ * The returned value is meaningless if {@link #getTheme()} returns
+ * <code>null</code>.
+ *
+ * @return true for project a theme, false for a framework theme
+ */
+ public boolean isProjectTheme() {
+ String theme = getTheme();
+ if (theme != null) {
+ assert theme.startsWith(STYLE_RESOURCE_PREFIX)
+ || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX);
+
+ return ResourceHelper.isProjectStyle(theme);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the current layout is locale-specific
+ *
+ * @return if this configuration represents a locale-specific layout
+ */
+ public boolean isLocaleSpecificLayout() {
+ return mEditedConfig == null || mEditedConfig.getLocaleQualifier() != null;
+ }
+
+ /**
+ * Returns the full, complete {@link FolderConfiguration}
+ *
+ * @return the full configuration
+ */
+ @NonNull
+ public FolderConfiguration getFullConfig() {
+ return mFullConfig;
+ }
+
+ /**
+ * Copies the full, complete {@link FolderConfiguration} into the given
+ * folder config instance.
+ *
+ * @param dest the {@link FolderConfiguration} instance to copy into
+ */
+ public void copyFullConfig(FolderConfiguration dest) {
+ dest.set(mFullConfig);
+ }
+
+ /**
+ * Returns the edited {@link FolderConfiguration} (this is not a full
+ * configuration, so you can think of it as the "constraints" used by the
+ * {@link ConfigurationMatcher} to produce a full configuration.
+ *
+ * @return the constraints configuration
+ */
+ @NonNull
+ public FolderConfiguration getEditedConfig() {
+ return mEditedConfig;
+ }
+
+ /**
+ * Sets the edited {@link FolderConfiguration} (this is not a full
+ * configuration, so you can think of it as the "constraints" used by the
+ * {@link ConfigurationMatcher} to produce a full configuration.
+ *
+ * @param editedConfig the constraints configuration
+ */
+ public void setEditedConfig(@NonNull FolderConfiguration editedConfig) {
+ mEditedConfig = editedConfig;
+ }
+
+ /**
+ * Sets the associated activity
+ *
+ * @param activity the activity
+ */
+ public void setActivity(String activity) {
+ mActivity = activity;
+ }
+
+ /**
+ * Sets the device
+ *
+ * @param device the device
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setDevice(Device device, boolean skipSync) {
+ mDevice = device;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the device state
+ *
+ * @param state the device state
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setDeviceState(State state, boolean skipSync) {
+ mState = state;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the locale
+ *
+ * @param locale the locale
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setLocale(@NonNull Locale locale, boolean skipSync) {
+ mLocale = locale;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the rendering target
+ *
+ * @param target rendering target
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setTarget(IAndroidTarget target, boolean skipSync) {
+ mTarget = target;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the display name to be shown for this configuration.
+ *
+ * @param displayName the new display name
+ */
+ public void setDisplayName(@Nullable String displayName) {
+ mDisplayName = displayName;
+ }
+
+ /**
+ * Sets the night mode
+ *
+ * @param night the night mode
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setNightMode(@NonNull NightMode night, boolean skipSync) {
+ mNightMode = night;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the UI mode
+ *
+ * @param uiMode the UI mode
+ * @param skipSync if true, don't sync folder configuration (typically because
+ * you are going to set other configuration parameters and you'll call
+ * {@link #syncFolderConfig()} once at the end)
+ */
+ public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) {
+ mUiMode = uiMode;
+
+ if (!skipSync) {
+ syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the theme style
+ *
+ * @param theme the theme
+ */
+ public void setTheme(String theme) {
+ mTheme = theme;
+ checkThemePrefix();
+ }
+
+ /**
+ * Updates the folder configuration such that it reflects changes in
+ * configuration state such as the device orientation, the UI mode, the
+ * rendering target, etc.
+ */
+ public void syncFolderConfig() {
+ Device device = getDevice();
+ if (device == null) {
+ return;
+ }
+
+ // get the device config from the device/state combos.
+ FolderConfiguration config = DeviceConfigHelper.getFolderConfig(getDeviceState());
+
+ // replace the config with the one from the device
+ mFullConfig.set(config);
+
+ // sync the selected locale
+ Locale locale = getLocale();
+ mFullConfig.setLocaleQualifier(locale.qualifier);
+ if (!locale.hasLanguage()) {
+ // Avoid getting the layout library if the locale doesn't have any language.
+ mFullConfig.setLayoutDirectionQualifier(
+ new LayoutDirectionQualifier(LayoutDirection.LTR));
+ } else {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ AndroidTargetData targetData = currentSdk.getTargetData(getTarget());
+ if (targetData != null) {
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ if (layoutLib != null) {
+ if (layoutLib.isRtl(locale.toLocaleId())) {
+ mFullConfig.setLayoutDirectionQualifier(
+ new LayoutDirectionQualifier(LayoutDirection.RTL));
+ } else {
+ mFullConfig.setLayoutDirectionQualifier(
+ new LayoutDirectionQualifier(LayoutDirection.LTR));
+ }
+ }
+ }
+ }
+ }
+
+ // Replace the UiMode with the selected one, if one is selected
+ UiMode uiMode = getUiMode();
+ if (uiMode != null) {
+ mFullConfig.setUiModeQualifier(new UiModeQualifier(uiMode));
+ }
+
+ // Replace the NightMode with the selected one, if one is selected
+ NightMode nightMode = getNightMode();
+ if (nightMode != null) {
+ mFullConfig.setNightModeQualifier(new NightModeQualifier(nightMode));
+ }
+
+ // replace the API level by the selection of the combo
+ IAndroidTarget target = getTarget();
+ if (target == null && mConfigChooser != null) {
+ target = mConfigChooser.getProjectTarget();
+ }
+ if (target != null) {
+ int apiLevel = target.getVersion().getApiLevel();
+ mFullConfig.setVersionQualifier(new VersionQualifier(apiLevel));
+ }
+ }
+
+ /**
+ * Creates a string suitable for persistence, which can be initialized back
+ * to a configuration via {@link #initialize(String)}
+ *
+ * @return a persistent string
+ */
+ @NonNull
+ public String toPersistentString() {
+ StringBuilder sb = new StringBuilder(32);
+ Device device = getDevice();
+ if (device != null) {
+ sb.append(device.getName());
+ sb.append(SEP);
+ State state = getDeviceState();
+ if (state != null) {
+ sb.append(state.getName());
+ }
+ sb.append(SEP);
+ Locale locale = getLocale();
+ if (isLocaleSpecificLayout() && locale != null && locale.qualifier.hasLanguage()) {
+ // locale[0]/[1] can be null sometimes when starting Eclipse
+ sb.append(locale.qualifier.getLanguage());
+ sb.append(SEP_LOCALE);
+ if (locale.qualifier.hasRegion()) {
+ sb.append(locale.qualifier.getRegion());
+ }
+ }
+ sb.append(SEP);
+ // Need to escape the theme: if we write the full theme style, then
+ // we can end up with ":"'s in the string (as in @android:style/Theme) which
+ // can be mistaken for {@link #SEP}. Instead use {@link #MARKER_FRAMEWORK}.
+ String theme = getTheme();
+ if (theme != null) {
+ String themeName = ResourceHelper.styleToTheme(theme);
+ if (theme.startsWith(STYLE_RESOURCE_PREFIX)) {
+ sb.append(MARKER_PROJECT);
+ } else if (theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
+ sb.append(MARKER_FRAMEWORK);
+ }
+ sb.append(themeName);
+ }
+ sb.append(SEP);
+ UiMode uiMode = getUiMode();
+ if (uiMode != null) {
+ sb.append(uiMode.getResourceValue());
+ }
+ sb.append(SEP);
+ NightMode nightMode = getNightMode();
+ if (nightMode != null) {
+ sb.append(nightMode.getResourceValue());
+ }
+ sb.append(SEP);
+
+ // We used to store the render target here in R9. Leave a marker
+ // to ensure that we don't reuse this slot; add new extra fields after it.
+ sb.append(SEP);
+ String activity = getActivity();
+ if (activity != null) {
+ sb.append(activity);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /** Returns the preferred theme, or null */
+ @Nullable
+ String computePreferredTheme() {
+ IProject project = mConfigChooser.getProject();
+ ManifestInfo manifest = ManifestInfo.get(project);
+
+ // Look up the screen size for the current state
+ ScreenSize screenSize = null;
+ Device device = getDevice();
+ if (device != null) {
+ List<State> states = device.getAllStates();
+ for (State state : states) {
+ FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(state);
+ if (folderConfig != null) {
+ ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier();
+ screenSize = qualifier.getValue();
+ break;
+ }
+ }
+ }
+
+ // Look up the default/fallback theme to use for this project (which
+ // depends on the screen size when no particular theme is specified
+ // in the manifest)
+ String defaultTheme = manifest.getDefaultTheme(getTarget(), screenSize);
+
+ String preferred = defaultTheme;
+ if (getTheme() == null) {
+ // If we are rendering a layout in included context, pick the theme
+ // from the outer layout instead
+
+ String activity = getActivity();
+ if (activity != null) {
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ preferred = attributes.getTheme();
+ }
+ }
+ if (preferred == null) {
+ preferred = defaultTheme;
+ }
+ setTheme(preferred);
+ }
+
+ return preferred;
+ }
+
+ private void checkThemePrefix() {
+ if (mTheme != null && !mTheme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (mTheme.isEmpty()) {
+ computePreferredTheme();
+ return;
+ }
+ ResourceRepository frameworkRes = mConfigChooser.getClient().getFrameworkResources();
+ if (frameworkRes != null
+ && frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + mTheme)) {
+ mTheme = ANDROID_STYLE_RESOURCE_PREFIX + mTheme;
+ } else {
+ mTheme = STYLE_RESOURCE_PREFIX + mTheme;
+ }
+ }
+ }
+
+ /**
+ * Initializes a string previously created with
+ * {@link #toPersistentString()}
+ *
+ * @param data the string to initialize back from
+ * @return true if the configuration was initialized
+ */
+ boolean initialize(String data) {
+ String[] values = data.split(SEP);
+ if (values.length >= 6 && values.length <= 8) {
+ for (Device d : mConfigChooser.getDevices()) {
+ if (d.getName().equals(values[0])) {
+ mDevice = d;
+ String stateName = null;
+ FolderConfiguration config = null;
+ if (!values[1].isEmpty() && !values[1].equals("null")) { //$NON-NLS-1$
+ stateName = values[1];
+ config = DeviceConfigHelper.getFolderConfig(mDevice, stateName);
+ } else if (mDevice.getAllStates().size() > 0) {
+ State first = mDevice.getAllStates().get(0);
+ stateName = first.getName();
+ config = DeviceConfigHelper.getFolderConfig(first);
+ }
+ mState = getState(mDevice, stateName);
+ if (config != null) {
+ // Load locale. Note that this can get overwritten by the
+ // project-wide settings read below.
+ LocaleQualifier locale = Locale.ANY_QUALIFIER;
+ String locales[] = values[2].split(SEP_LOCALE);
+ if (locales.length >= 2 && locales[0].length() > 0
+ && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) {
+ String language = locales[0];
+ String region = locales[1];
+ if (region.length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(region)) {
+ locale = LocaleQualifier.getQualifier(language + "-r" + region);
+ } else {
+ locale = new LocaleQualifier(language);
+ }
+ mLocale = Locale.create(locale);
+ }
+
+ // Decode the theme name: See {@link #getData}
+ mTheme = values[3];
+ if (mTheme.startsWith(MARKER_FRAMEWORK)) {
+ mTheme = ANDROID_STYLE_RESOURCE_PREFIX
+ + mTheme.substring(MARKER_FRAMEWORK.length());
+ } else if (mTheme.startsWith(MARKER_PROJECT)) {
+ mTheme = STYLE_RESOURCE_PREFIX
+ + mTheme.substring(MARKER_PROJECT.length());
+ } else {
+ checkThemePrefix();
+ }
+
+ mUiMode = UiMode.getEnum(values[4]);
+ if (mUiMode == null) {
+ mUiMode = UiMode.NORMAL;
+ }
+ mNightMode = NightMode.getEnum(values[5]);
+ if (mNightMode == null) {
+ mNightMode = NightMode.NOTNIGHT;
+ }
+
+ // element 7/values[6]: used to store render target in R9.
+ // No longer stored here. If adding more data, make
+ // sure you leave 7 alone.
+
+ Pair<Locale, IAndroidTarget> pair = loadRenderState(mConfigChooser);
+ if (pair != null) {
+ // We only use the "global" setting
+ if (!isLocaleSpecificLayout()) {
+ mLocale = pair.getFirst();
+ }
+ mTarget = pair.getSecond();
+ }
+
+ if (values.length == 8) {
+ mActivity = values[7];
+ }
+
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Loads the render state (the locale and the render target, which are shared among
+ * all the layouts meaning that changing it in one will change it in all) and returns
+ * the current project-wide locale and render target to be used.
+ *
+ * @param chooser the {@link ConfigurationChooser} providing information about
+ * loaded targets
+ * @return a pair of a locale and a render target
+ */
+ @Nullable
+ static Pair<Locale, IAndroidTarget> loadRenderState(ConfigurationChooser chooser) {
+ IProject project = chooser.getProject();
+ if (project == null || !project.isAccessible()) {
+ return null;
+ }
+
+ try {
+ String data = project.getPersistentProperty(NAME_RENDER_STATE);
+ if (data != null) {
+ Locale locale = Locale.ANY;
+ IAndroidTarget target = null;
+
+ String[] values = data.split(SEP);
+ if (values.length == 2) {
+
+ LocaleQualifier qualifier = Locale.ANY_QUALIFIER;
+ String locales[] = values[0].split(SEP_LOCALE);
+ if (locales.length >= 2 && locales[0].length() > 0
+ && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) {
+ String language = locales[0];
+ String region = locales[1];
+ if (region.length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(region)) {
+ locale = Locale.create(LocaleQualifier.getQualifier(language + "-r" + region));
+ } else {
+ locale = Locale.create(new LocaleQualifier(language));
+ }
+ } else {
+ locale = Locale.ANY;
+ }
+ if (AdtPrefs.getPrefs().isAutoPickRenderTarget()) {
+ target = ConfigurationMatcher.findDefaultRenderTarget(chooser);
+ } else {
+ String targetString = values[1];
+ target = stringToTarget(chooser, targetString);
+ // See if we should "correct" the rendering target to a
+ // better version. If you're using a pre-release version
+ // of the render target, and a final release is
+ // available and installed, we should switch to that
+ // one instead.
+ if (target != null) {
+ AndroidVersion version = target.getVersion();
+ List<IAndroidTarget> targetList = chooser.getTargetList();
+ if (version.getCodename() != null && targetList != null) {
+ int targetApiLevel = version.getApiLevel() + 1;
+ for (IAndroidTarget t : targetList) {
+ if (t.getVersion().getApiLevel() == targetApiLevel
+ && t.isPlatform()) {
+ target = t;
+ break;
+ }
+ }
+ }
+ } else {
+ target = ConfigurationMatcher.findDefaultRenderTarget(chooser);
+ }
+ }
+ }
+
+ return Pair.of(locale, target);
+ }
+
+ return Pair.of(Locale.ANY, ConfigurationMatcher.findDefaultRenderTarget(chooser));
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Saves the render state (the current locale and render target settings) into the
+ * project wide settings storage
+ */
+ void saveRenderState() {
+ IProject project = mConfigChooser.getProject();
+ if (project == null) {
+ return;
+ }
+ try {
+ // Generate a persistent string from locale+target
+ StringBuilder sb = new StringBuilder(32);
+ Locale locale = getLocale();
+ if (locale != null) {
+ // locale[0]/[1] can be null sometimes when starting Eclipse
+ sb.append(locale.qualifier.getLanguage());
+ sb.append(SEP_LOCALE);
+ if (locale.qualifier.hasRegion()) {
+ sb.append(locale.qualifier.getRegion());
+ }
+ }
+ sb.append(SEP);
+ IAndroidTarget target = getTarget();
+ if (target != null) {
+ sb.append(targetToString(target));
+ sb.append(SEP);
+ }
+
+ project.setPersistentProperty(NAME_RENDER_STATE, sb.toString());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ /**
+ * Returns a String id to represent an {@link IAndroidTarget} which can be translated
+ * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id
+ * will never contain the {@link #SEP} character.
+ *
+ * @param target the target to return an id for
+ * @return an id for the given target; never null
+ */
+ @NonNull
+ public static String targetToString(@NonNull IAndroidTarget target) {
+ return target.getFullName().replace(SEP, ""); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns an {@link IAndroidTarget} that corresponds to the given id that was
+ * originally returned by {@link #targetToString}. May be null, if the platform is no
+ * longer available, or if the platform list has not yet been initialized.
+ *
+ * @param chooser the {@link ConfigurationChooser} providing information about
+ * loaded targets
+ * @param id the id that corresponds to the desired platform
+ * @return an {@link IAndroidTarget} that matches the given id, or null
+ */
+ @Nullable
+ public static IAndroidTarget stringToTarget(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull String id) {
+ List<IAndroidTarget> targetList = chooser.getTargetList();
+ if (targetList != null && targetList.size() > 0) {
+ for (IAndroidTarget target : targetList) {
+ if (id.equals(targetToString(target))) {
+ return target;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns an {@link IAndroidTarget} that corresponds to the given id that was
+ * originally returned by {@link #targetToString}. May be null, if the platform is no
+ * longer available, or if the platform list has not yet been initialized.
+ *
+ * @param id the id that corresponds to the desired platform
+ * @return an {@link IAndroidTarget} that matches the given id, or null
+ */
+ @Nullable
+ public static IAndroidTarget stringToTarget(
+ @NonNull String id) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget[] targets = currentSdk.getTargets();
+ for (IAndroidTarget target : targets) {
+ if (id.equals(targetToString(target))) {
+ return target;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link State} by the given name for the given {@link Device}
+ *
+ * @param device the device
+ * @param name the name of the state
+ */
+ @Nullable
+ static State getState(@Nullable Device device, @Nullable String name) {
+ if (device == null) {
+ return null;
+ } else if (name != null) {
+ State state = device.getState(name);
+ if (state != null) {
+ return state;
+ }
+ }
+
+ return device.getDefaultState();
+ }
+
+ /**
+ * Returns the currently selected {@link Density}. This is guaranteed to be non null.
+ *
+ * @return the density
+ */
+ @NonNull
+ public Density getDensity() {
+ if (mFullConfig != null) {
+ DensityQualifier qual = mFullConfig.getDensityQualifier();
+ if (qual != null) {
+ // just a sanity check
+ Density d = qual.getValue();
+ if (d != Density.NODPI) {
+ return d;
+ }
+ }
+ }
+
+ // no config? return medium as the default density.
+ return Density.MEDIUM;
+ }
+
+ /**
+ * Get the next cyclical state after the given state
+ *
+ * @param from the state to start with
+ * @return the following state following
+ */
+ @Nullable
+ public State getNextDeviceState(@Nullable State from) {
+ Device device = getDevice();
+ if (device == null) {
+ return null;
+ }
+ List<State> states = device.getAllStates();
+ for (int i = 0; i < states.size(); i++) {
+ if (states.get(i) == from) {
+ return states.get((i + 1) % states.size());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if this configuration supports the given rendering
+ * capability
+ *
+ * @param capability the capability to check
+ * @return true if the capability is supported
+ */
+ public boolean supports(Capability capability) {
+ IAndroidTarget target = getTarget();
+ if (target != null) {
+ return RenderService.supports(target, capability);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this.getClass())
+ .add("display", getDisplayName()) //$NON-NLS-1$
+ .add("persistent", toPersistentString()) //$NON-NLS-1$
+ .toString();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java
new file mode 100644
index 000000000..009b8646c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java
@@ -0,0 +1,2096 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.RES_QUALIFIER_SEP;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.ide.eclipse.adt.AdtUtils.isUiThread;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_LOCALE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_THEME;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL;
+import static com.google.common.base.Objects.equal;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.StyleResourceValue;
+import com.android.ide.common.resources.LocaleManager;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.common.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.devices.DeviceManager.DevicesChangedListener;
+import com.android.sdklib.devices.State;
+import com.android.utils.Pair;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.ui.IEditorPart;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+
+/**
+ * The {@linkplain ConfigurationChooser} allows the user to pick a
+ * {@link Configuration} by configuring various constraints.
+ */
+public class ConfigurationChooser extends Composite
+ implements DevicesChangedListener, DisposeListener {
+ private static final String ICON_SQUARE = "square"; //$NON-NLS-1$
+ private static final String ICON_LANDSCAPE = "landscape"; //$NON-NLS-1$
+ private static final String ICON_PORTRAIT = "portrait"; //$NON-NLS-1$
+ private static final String ICON_LANDSCAPE_FLIP = "flip_landscape";//$NON-NLS-1$
+ private static final String ICON_PORTRAIT_FLIP = "flip_portrait";//$NON-NLS-1$
+ private static final String ICON_DISPLAY = "display"; //$NON-NLS-1$
+ private static final String ICON_THEMES = "themes"; //$NON-NLS-1$
+ private static final String ICON_ACTIVITY = "activity"; //$NON-NLS-1$
+
+ /** The configuration state associated with this editor */
+ private @NonNull Configuration mConfiguration = Configuration.create(this);
+
+ /** Serialized state to use when initializing the configuration after the SDK is loaded */
+ private String mInitialState;
+
+ /** The client of the configuration editor */
+ private final ConfigurationClient mClient;
+
+ /** Counter for programmatic UI changes: if greater than 0, we're within a call */
+ private int mDisableUpdates = 0;
+
+ /** List of available devices */
+ private Collection<Device> mDevices = Collections.emptyList();
+
+ /** List of available targets */
+ private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>();
+
+ /** List of available themes */
+ private final List<String> mThemeList = new ArrayList<String>();
+
+ /** List of available locales */
+ private final List<Locale > mLocaleList = new ArrayList<Locale>();
+
+ /** The file being edited */
+ private IFile mEditedFile;
+
+ /** The {@link ProjectResources} for the edited file's project */
+ private ProjectResources mResources;
+
+ /** The target of the project of the file being edited. */
+ private IAndroidTarget mProjectTarget;
+
+ /** Dropdown for configurations */
+ private ToolItem mConfigCombo;
+
+ /** Dropdown for devices */
+ private ToolItem mDeviceCombo;
+
+ /** Dropdown for device states */
+ private ToolItem mOrientationCombo;
+
+ /** Dropdown for themes */
+ private ToolItem mThemeCombo;
+
+ /** Dropdown for locales */
+ private ToolItem mLocaleCombo;
+
+ /** Dropdown for activities */
+ private ToolItem mActivityCombo;
+
+ /** Dropdown for rendering targets */
+ private ToolItem mTargetCombo;
+
+ /** Whether the SDK has changed since the last model reload; if so we must reload targets */
+ private boolean mSdkChanged = true;
+
+ /**
+ * Creates a new {@linkplain ConfigurationChooser} and adds it to the
+ * parent. The method also receives custom buttons to set into the
+ * configuration composite. The list is organized as an array of arrays.
+ * Each array represents a group of buttons thematically grouped together.
+ *
+ * @param client the client embedding this configuration chooser
+ * @param parent The parent composite.
+ * @param initialState The initial state (serialized form) to use for the
+ * configuration
+ */
+ public ConfigurationChooser(
+ @NonNull ConfigurationClient client,
+ Composite parent,
+ @Nullable String initialState) {
+ super(parent, SWT.NONE);
+ mClient = client;
+
+ setVisible(false); // Delayed until the targets are loaded
+
+ mInitialState = initialState;
+ setLayout(new GridLayout(1, false));
+
+ IconFactory icons = IconFactory.getInstance();
+
+ // TODO: Consider switching to a CoolBar instead
+ ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+ toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+ mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN );
+ mConfigCombo.setImage(icons.getIcon("android_file")); //$NON-NLS-1$
+ mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse");
+
+ @SuppressWarnings("unused")
+ ToolItem separator2 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mDeviceCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mDeviceCombo.setImage(icons.getIcon(ICON_DISPLAY));
+
+ @SuppressWarnings("unused")
+ ToolItem separator3 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mOrientationCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mOrientationCombo.setImage(icons.getIcon(ICON_PORTRAIT));
+ mOrientationCombo.setToolTipText("Go to next state");
+
+ @SuppressWarnings("unused")
+ ToolItem separator4 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mThemeCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mThemeCombo.setImage(icons.getIcon(ICON_THEMES));
+
+ @SuppressWarnings("unused")
+ ToolItem separator5 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mActivityCombo = new ToolItem(toolBar, SWT.DROP_DOWN);
+ mActivityCombo.setToolTipText("Associated activity or fragment providing context");
+ // The JDT class icon is lopsided, presumably because they've left room in the
+ // bottom right corner for badges (for static, final etc). Unfortunately, this
+ // means that the icon looks out of place when sitting close to the language globe
+ // icon, the theme icon, etc so that it looks vertically misaligned:
+ //mActivityCombo.setImage(JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS));
+ // ...so use one that is centered instead:
+ mActivityCombo.setImage(icons.getIcon(ICON_ACTIVITY));
+
+ @SuppressWarnings("unused")
+ ToolItem separator6 = new ToolItem(toolBar, SWT.SEPARATOR);
+
+ //ToolBar rightToolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+ //rightToolBar.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
+ ToolBar rightToolBar = toolBar;
+
+ mLocaleCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN);
+ mLocaleCombo.setImage(FlagManager.getGlobeIcon());
+ mLocaleCombo.setToolTipText("Locale to use when rendering layouts in Eclipse");
+
+ @SuppressWarnings("unused")
+ ToolItem separator7 = new ToolItem(rightToolBar, SWT.SEPARATOR);
+
+ mTargetCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN);
+ mTargetCombo.setImage(AdtPlugin.getAndroidLogo());
+ mTargetCombo.setToolTipText("Android version to use when rendering layouts in Eclipse");
+
+ SelectionListener listener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+
+ if (source == mConfigCombo) {
+ ConfigurationMenuListener.show(ConfigurationChooser.this, mConfigCombo);
+ } else if (source == mActivityCombo) {
+ ActivityMenuListener.show(ConfigurationChooser.this, mActivityCombo);
+ } else if (source == mLocaleCombo) {
+ LocaleMenuListener.show(ConfigurationChooser.this, mLocaleCombo);
+ } else if (source == mDeviceCombo) {
+ DeviceMenuListener.show(ConfigurationChooser.this, mDeviceCombo);
+ } else if (source == mTargetCombo) {
+ TargetMenuListener.show(ConfigurationChooser.this, mTargetCombo);
+ } else if (source == mThemeCombo) {
+ ThemeMenuAction.showThemeMenu(ConfigurationChooser.this, mThemeCombo,
+ mThemeList);
+ } else if (source == mOrientationCombo) {
+ if (e.detail == SWT.ARROW) {
+ OrientationMenuAction.showMenu(ConfigurationChooser.this,
+ mOrientationCombo);
+ } else {
+ gotoNextState();
+ }
+ }
+ }
+ };
+ mConfigCombo.addSelectionListener(listener);
+ mActivityCombo.addSelectionListener(listener);
+ mLocaleCombo.addSelectionListener(listener);
+ mDeviceCombo.addSelectionListener(listener);
+ mTargetCombo.addSelectionListener(listener);
+ mThemeCombo.addSelectionListener(listener);
+ mOrientationCombo.addSelectionListener(listener);
+
+ addDisposeListener(this);
+
+ initDevices();
+ initTargets();
+ }
+
+ /**
+ * Returns the edited file
+ *
+ * @return the file
+ */
+ @Nullable
+ public IFile getEditedFile() {
+ return mEditedFile;
+ }
+
+ /**
+ * Returns the project of the edited file
+ *
+ * @return the project
+ */
+ @Nullable
+ public IProject getProject() {
+ if (mEditedFile != null) {
+ return mEditedFile.getProject();
+ } else {
+ return null;
+ }
+ }
+
+ ConfigurationClient getClient() {
+ return mClient;
+ }
+
+ /**
+ * Returns the project resources for the project being configured by this
+ * chooser
+ *
+ * @return the project resources
+ */
+ @Nullable
+ public ProjectResources getResources() {
+ return mResources;
+ }
+
+ /**
+ * Returns the full, complete {@link FolderConfiguration}
+ *
+ * @return the full configuration
+ */
+ public FolderConfiguration getFullConfiguration() {
+ return mConfiguration.getFullConfig();
+ }
+
+ /**
+ * Returns the project target
+ *
+ * @return the project target
+ */
+ public IAndroidTarget getProjectTarget() {
+ return mProjectTarget;
+ }
+
+ /**
+ * Returns the configuration being edited by this {@linkplain ConfigurationChooser}
+ *
+ * @return the configuration
+ */
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /**
+ * Returns the list of locales
+ * @return a list of {@link ResourceQualifier} pairs
+ */
+ @NonNull
+ public List<Locale> getLocaleList() {
+ return mLocaleList;
+ }
+
+ /**
+ * Returns the list of available devices
+ *
+ * @return a list of {@link Device} objects
+ */
+ @NonNull
+ public Collection<Device> getDevices() {
+ return mDevices;
+ }
+
+ /**
+ * Returns the list of available render targets
+ *
+ * @return a list of {@link IAndroidTarget} objects
+ */
+ @NonNull
+ public List<IAndroidTarget> getTargetList() {
+ return mTargetList;
+ }
+
+ // ---- Configuration State Lookup ----
+
+ /**
+ * Returns the rendering target to be used
+ *
+ * @return the target
+ */
+ @NonNull
+ public IAndroidTarget getTarget() {
+ IAndroidTarget target = mConfiguration.getTarget();
+ if (target == null) {
+ target = mProjectTarget;
+ }
+
+ return target;
+ }
+
+ /**
+ * Returns the current device string, or null if no device is selected
+ *
+ * @return the device name, or null
+ */
+ @Nullable
+ public String getDeviceName() {
+ Device device = mConfiguration.getDevice();
+ if (device != null) {
+ return device.getName();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the current theme, or null if none has been selected
+ *
+ * @return the theme name, or null
+ */
+ @Nullable
+ public String getThemeName() {
+ String theme = mConfiguration.getTheme();
+ if (theme != null) {
+ theme = ResourceHelper.styleToTheme(theme);
+ }
+
+ return theme;
+ }
+
+ /** Move to the next device state, changing the icon if it changes orientation */
+ private void gotoNextState() {
+ State state = mConfiguration.getDeviceState();
+ State flipped = mConfiguration.getNextDeviceState(state);
+ if (flipped != state) {
+ selectDeviceState(flipped);
+ onDeviceConfigChange();
+ }
+ }
+
+ // ---- Implements DisposeListener ----
+
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ dispose();
+ }
+
+ @Override
+ public void dispose() {
+ if (!isDisposed()) {
+ super.dispose();
+
+ final Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ DeviceManager manager = sdk.getDeviceManager();
+ manager.unregisterListener(this);
+ }
+ }
+ }
+
+ // ---- Init and reset/reload methods ----
+
+ /**
+ * Sets the reference to the file being edited.
+ * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is
+ * loaded (or reloaded as the SDK/target changes).
+ *
+ * @param file the file being opened
+ *
+ * @see #onXmlModelLoaded()
+ * @see #replaceFile(IFile)
+ * @see #changeFileOnNewConfig(IFile)
+ */
+ public void setFile(IFile file) {
+ mEditedFile = file;
+ ensureInitialized();
+ }
+
+ /**
+ * Replaces the UI with a given file configuration. This is meant to answer the user
+ * explicitly opening a different version of the same layout from the Package Explorer.
+ * <p/>This attempts to keep the current config, but may change it if it's not compatible or
+ * not the best match
+ * @param file the file being opened.
+ */
+ public void replaceFile(IFile file) {
+ // if there is no previous selection, revert to default mode.
+ if (mConfiguration.getDevice() == null) {
+ setFile(file); // onTargetChanged will be called later.
+ return;
+ }
+
+ setFile(file);
+ IProject project = mEditedFile.getProject();
+ mResources = ResourceManager.getInstance().getProjectResources(project);
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ mConfiguration.setEditedConfig(resFolder.getConfiguration());
+
+ mDisableUpdates++; // we do not want to trigger onXXXChange when setting
+ // new values in the widgets.
+
+ try {
+ // only attempt to do anything if the SDK and targets are loaded.
+ LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
+
+ if (sdkStatus == LoadStatus.LOADED) {
+ setVisible(true);
+
+ LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget,
+ null /*project*/);
+
+ if (targetStatus == LoadStatus.LOADED) {
+
+ // update the current config selection to make sure it's
+ // compatible with the new file
+ ConfigurationMatcher matcher = new ConfigurationMatcher(this);
+ matcher.adaptConfigSelection(true /*needBestMatch*/);
+ mConfiguration.syncFolderConfig();
+
+ // update the string showing the config value
+ selectConfiguration(mConfiguration.getEditedConfig());
+ updateActivity();
+ }
+ } else if (sdkStatus == LoadStatus.FAILED) {
+ setVisible(true);
+ }
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ /**
+ * Updates the UI with a new file that was opened in response to a config change.
+ * @param file the file being opened.
+ *
+ * @see #replaceFile(IFile)
+ */
+ public void changeFileOnNewConfig(IFile file) {
+ setFile(file);
+ IProject project = mEditedFile.getProject();
+ mResources = ResourceManager.getInstance().getProjectResources(project);
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ FolderConfiguration config = resFolder.getConfiguration();
+ mConfiguration.setEditedConfig(config);
+
+ // All that's needed is to update the string showing the config value
+ // (since the config combo settings chosen by the user).
+ selectConfiguration(config);
+ }
+
+ /**
+ * Resets the configuration chooser to reflect the given file configuration. This is
+ * intended to be used by the "Show Included In" functionality where the user has
+ * picked a non-default configuration (such as a particular landscape layout) and the
+ * configuration chooser must be switched to a landscape layout. This method will
+ * trigger a model change.
+ * <p>
+ * This will NOT trigger a redraw event!
+ * <p>
+ * FIXME: We are currently setting the configuration file to be the configuration for
+ * the "outer" (the including) file, rather than the inner file, which is the file the
+ * user is actually editing. We need to refine this, possibly with a way for the user
+ * to choose which configuration they are editing. And in particular, we should be
+ * filtering the configuration chooser to only show options in the outer configuration
+ * that are compatible with the inner included file.
+ *
+ * @param file the file to be configured
+ */
+ public void resetConfigFor(IFile file) {
+ setFile(file);
+
+ IFolder parent = (IFolder) mEditedFile.getParent();
+ ResourceFolder resFolder = mResources.getResourceFolder(parent);
+ if (resFolder != null) {
+ mConfiguration.setEditedConfig(resFolder.getConfiguration());
+ } else {
+ FolderConfiguration config = FolderConfiguration.getConfig(
+ parent.getName().split(RES_QUALIFIER_SEP));
+ if (config != null) {
+ mConfiguration.setEditedConfig(config);
+ } else {
+ mConfiguration.setEditedConfig(new FolderConfiguration());
+ }
+ }
+
+ onXmlModelLoaded();
+ }
+
+
+ /**
+ * Sets the current configuration to match the given folder configuration,
+ * the given theme name, the given device and device state.
+ *
+ * @param configuration new folder configuration to use
+ */
+ public void setConfiguration(@NonNull Configuration configuration) {
+ if (mClient != null) {
+ mClient.aboutToChange(MASK_ALL);
+ }
+
+ Configuration oldConfiguration = mConfiguration;
+ mConfiguration = configuration;
+ mConfiguration.setChooser(this);
+
+ selectTheme(configuration.getTheme());
+ selectLocale(configuration.getLocale());
+ selectDevice(configuration.getDevice());
+ selectDeviceState(configuration.getDeviceState());
+ selectTarget(configuration.getTarget());
+ selectActivity(configuration.getActivity());
+
+ // This may be a second refresh after triggered by theme above
+ if (mClient != null) {
+ LayoutCanvas canvas = mClient.getCanvas();
+ if (canvas != null) {
+ assert mConfiguration != oldConfiguration;
+ canvas.getPreviewManager().updateChooserConfig(oldConfiguration, mConfiguration);
+ }
+
+ boolean accepted = mClient.changed(MASK_ALL);
+ if (!accepted) {
+ configuration = oldConfiguration;
+ selectTheme(configuration.getTheme());
+ selectLocale(configuration.getLocale());
+ selectDevice(configuration.getDevice());
+ selectDeviceState(configuration.getDeviceState());
+ selectTarget(configuration.getTarget());
+ selectActivity(configuration.getActivity());
+ if (canvas != null && mConfiguration != oldConfiguration) {
+ canvas.getPreviewManager().updateChooserConfig(mConfiguration,
+ oldConfiguration);
+ }
+ return;
+ } else {
+ int changed = 0;
+ if (!equal(oldConfiguration.getTheme(), mConfiguration.getTheme())) {
+ changed |= CFG_THEME;
+ }
+ if (!equal(oldConfiguration.getDevice(), mConfiguration.getDevice())) {
+ changed |= CFG_DEVICE | CFG_DEVICE_STATE;
+ }
+ if (changed != 0) {
+ syncToVariations(changed, mEditedFile, mConfiguration, false, true);
+ }
+ }
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Responds to the event that the basic SDK information finished loading.
+ * @param target the possibly new target object associated with the file being edited (in case
+ * the SDK path was changed).
+ */
+ public void onSdkLoaded(IAndroidTarget target) {
+ // a change to the SDK means that we need to check for new/removed devices.
+ mSdkChanged = true;
+
+ // store the new target.
+ mProjectTarget = target;
+
+ mDisableUpdates++; // we do not want to trigger onXXXChange when setting
+ // new values in the widgets.
+ try {
+ updateDevices();
+ updateTargets();
+ ensureInitialized();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ /**
+ * Responds to the XML model being loaded, either the first time or when the
+ * Target/SDK changes.
+ * <p>
+ * This initializes the UI, either with the first compatible configuration
+ * found, or it will attempt to restore a configuration if one is found to
+ * have been saved in the file persistent storage.
+ * <p>
+ * If the SDK or target are not loaded, nothing will happen (but the method
+ * must be called back when they are.)
+ * <p>
+ * The method automatically handles being called the first time after editor
+ * creation, or being called after during SDK/Target changes (as long as
+ * {@link #onSdkLoaded(IAndroidTarget)} is properly called).
+ *
+ * @return the target data for the rendering target used to render the
+ * layout
+ *
+ * @see #saveConstraints()
+ * @see #onSdkLoaded(IAndroidTarget)
+ */
+ public AndroidTargetData onXmlModelLoaded() {
+ AndroidTargetData targetData = null;
+
+ // only attempt to do anything if the SDK and targets are loaded.
+ LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
+ if (sdkStatus == LoadStatus.LOADED) {
+ mDisableUpdates++; // we do not want to trigger onXXXChange when setting
+
+ try {
+ // init the devices if needed (new SDK or first time going through here)
+ if (mSdkChanged) {
+ updateDevices();
+ updateTargets();
+ ensureInitialized();
+ mSdkChanged = false;
+ }
+
+ IProject project = mEditedFile.getProject();
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ mProjectTarget = currentSdk.getTarget(project);
+ }
+
+ LoadStatus targetStatus = LoadStatus.FAILED;
+ if (mProjectTarget != null) {
+ targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null);
+ updateTargets();
+ ensureInitialized();
+ }
+
+ if (targetStatus == LoadStatus.LOADED) {
+ setVisible(true);
+ if (mResources == null) {
+ mResources = ResourceManager.getInstance().getProjectResources(project);
+ }
+ if (mConfiguration.getEditedConfig() == null) {
+ IFolder parent = (IFolder) mEditedFile.getParent();
+ ResourceFolder resFolder = mResources.getResourceFolder(parent);
+ if (resFolder != null) {
+ mConfiguration.setEditedConfig(resFolder.getConfiguration());
+ } else {
+ FolderConfiguration config = FolderConfiguration.getConfig(
+ parent.getName().split(RES_QUALIFIER_SEP));
+ if (config != null) {
+ mConfiguration.setEditedConfig(config);
+ } else {
+ mConfiguration.setEditedConfig(new FolderConfiguration());
+ }
+ }
+ }
+
+ targetData = Sdk.getCurrent().getTargetData(mProjectTarget);
+
+ // get the file stored state
+ ensureInitialized();
+ boolean loadedConfigData = mConfiguration.getDevice() != null &&
+ mConfiguration.getDeviceState() != null;
+
+ // Load locale list. This must be run after we initialize the
+ // configuration above, since it attempts to sync the UI with
+ // the value loaded into the configuration.
+ updateLocales();
+
+ // If the current state was loaded from the persistent storage, we update the
+ // UI with it and then try to adapt it (which will handle incompatible
+ // configuration).
+ // Otherwise, just look for the first compatible configuration.
+ ConfigurationMatcher matcher = new ConfigurationMatcher(this);
+ if (loadedConfigData) {
+ // first make sure we have the config to adapt
+ selectDevice(mConfiguration.getDevice());
+ selectDeviceState(mConfiguration.getDeviceState());
+ mConfiguration.syncFolderConfig();
+
+ matcher.adaptConfigSelection(false);
+
+ IAndroidTarget target = mConfiguration.getTarget();
+ selectTarget(target);
+ targetData = Sdk.getCurrent().getTargetData(target);
+ } else {
+ matcher.findAndSetCompatibleConfig(false);
+
+ // Default to modern layout lib
+ IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(this);
+ if (target != null) {
+ targetData = Sdk.getCurrent().getTargetData(target);
+ selectTarget(target);
+ mConfiguration.setTarget(target, true);
+ }
+ }
+
+ // Update activity: This is done before updateThemes() since
+ // the themes selection can depend on the currently selected activity
+ // (e.g. when there are manifest registrations for the theme to use
+ // for a given activity)
+ updateActivity();
+
+ // Update themes. This is done after updating the devices above,
+ // since we want to look at the chosen device size to decide
+ // what the default theme (for example, with Honeycomb we choose
+ // Holo as the default theme but only if the screen size is XLARGE
+ // (and of course only if the manifest does not specify another
+ // default theme).
+ updateThemes();
+
+ // update the string showing the config value
+ selectConfiguration(mConfiguration.getEditedConfig());
+
+ // compute the final current config
+ mConfiguration.syncFolderConfig();
+ } else if (targetStatus == LoadStatus.FAILED) {
+ setVisible(true);
+ }
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ return targetData;
+ }
+
+ /**
+ * This is a temporary workaround for a infrequently happening bug; apparently
+ * there are cases where the configuration chooser isn't shown
+ */
+ public void ensureVisible() {
+ if (!isVisible()) {
+ LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus();
+ if (sdkStatus == LoadStatus.LOADED) {
+ onXmlModelLoaded();
+ }
+ }
+ }
+
+ /**
+ * An alternate layout for this layout has been created. This means that the
+ * current layout may no longer be a best fit. However, since we support multiple
+ * layouts being open at the same time, we need to adjust the current configuration
+ * back to something where this layout <b>is</b> a best match.
+ */
+ public void onAlternateLayoutCreated() {
+ IFile best = ConfigurationMatcher.getBestFileMatch(this);
+ if (best != null && !best.equals(mEditedFile)) {
+ ConfigurationMatcher matcher = new ConfigurationMatcher(this);
+ matcher.adaptConfigSelection(true /*needBestMatch*/);
+ mConfiguration.syncFolderConfig();
+ if (mClient != null) {
+ mClient.changed(MASK_ALL);
+ }
+ }
+ }
+
+ /**
+ * Loads the list of {@link Device}s and inits the UI with it.
+ */
+ private void initDevices() {
+ final Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ DeviceManager manager = sdk.getDeviceManager();
+ // This method can be called more than once, so avoid duplicate entries
+ manager.unregisterListener(this);
+ manager.registerListener(this);
+ mDevices = manager.getDevices(DeviceManager.ALL_DEVICES);
+ } else {
+ mDevices = new ArrayList<Device>();
+ }
+ }
+
+ /**
+ * Loads the list of {@link IAndroidTarget} and inits the UI with it.
+ */
+ private boolean initTargets() {
+ mTargetList.clear();
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget[] targets = currentSdk.getTargets();
+ for (int i = 0 ; i < targets.length; i++) {
+ if (targets[i].hasRenderingLibrary()) {
+ mTargetList.add(targets[i]);
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Ensures that the configuration has been initialized */
+ public void ensureInitialized() {
+ if (mConfiguration.getDevice() == null && mEditedFile != null) {
+ String data = ConfigurationDescription.getDescription(mEditedFile);
+ if (mInitialState != null) {
+ data = mInitialState;
+ mInitialState = null;
+ }
+ if (data != null) {
+ mConfiguration.initialize(data);
+ mConfiguration.syncFolderConfig();
+ }
+ }
+ }
+
+ private void updateDevices() {
+ if (mDevices.size() == 0) {
+ initDevices();
+ }
+ }
+
+ private void updateTargets() {
+ if (mTargetList.size() == 0) {
+ if (!initTargets()) {
+ return;
+ }
+ }
+
+ IAndroidTarget renderingTarget = mConfiguration.getTarget();
+
+ IAndroidTarget match = null;
+ for (IAndroidTarget target : mTargetList) {
+ if (renderingTarget != null) {
+ // use equals because the rendering could be from a previous SDK, so
+ // it may not be the same instance.
+ if (renderingTarget.equals(target)) {
+ match = target;
+ }
+ } else if (mProjectTarget == target) {
+ match = target;
+ }
+
+ }
+
+ if (match == null) {
+ // the rendering target is the same as the project.
+ renderingTarget = mProjectTarget;
+ } else {
+ // set the rendering target to the new object.
+ renderingTarget = match;
+ }
+
+ mConfiguration.setTarget(renderingTarget, true);
+ selectTarget(renderingTarget);
+ }
+
+ /** Update the toolbar whenever a label has changed, to not only
+ * cause the layout in the current toolbar to update, but to possibly
+ * wrap the toolbars and update the layout of the surrounding area.
+ */
+ private void resizeToolBar() {
+ Point size = getSize();
+ Point newSize = computeSize(size.x, SWT.DEFAULT, true);
+ setSize(newSize);
+ Composite parent = getParent();
+ parent.layout();
+ parent.redraw();
+ }
+
+
+ Image getOrientationIcon(ScreenOrientation orientation, boolean flip) {
+ IconFactory icons = IconFactory.getInstance();
+ switch (orientation) {
+ case LANDSCAPE:
+ return icons.getIcon(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE);
+ case SQUARE:
+ return icons.getIcon(ICON_SQUARE);
+ case PORTRAIT:
+ default:
+ return icons.getIcon(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT);
+ }
+ }
+
+ ImageDescriptor getOrientationImage(ScreenOrientation orientation, boolean flip) {
+ IconFactory icons = IconFactory.getInstance();
+ switch (orientation) {
+ case LANDSCAPE:
+ return icons.getImageDescriptor(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE);
+ case SQUARE:
+ return icons.getImageDescriptor(ICON_SQUARE);
+ case PORTRAIT:
+ default:
+ return icons.getImageDescriptor(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT);
+ }
+ }
+
+ @NonNull
+ ScreenOrientation getOrientation(State state) {
+ FolderConfiguration config = DeviceConfigHelper.getFolderConfig(state);
+ ScreenOrientation orientation = null;
+ if (config != null && config.getScreenOrientationQualifier() != null) {
+ orientation = config.getScreenOrientationQualifier().getValue();
+ }
+
+ if (orientation == null) {
+ orientation = ScreenOrientation.PORTRAIT;
+ }
+
+ return orientation;
+ }
+
+ /**
+ * Stores the current config selection into the edited file such that we can
+ * bring it back the next time this layout is opened.
+ */
+ public void saveConstraints() {
+ String description = mConfiguration.toPersistentString();
+ if (description != null && !description.isEmpty()) {
+ ConfigurationDescription.setDescription(mEditedFile, description);
+ }
+ }
+
+ // ---- Setting the current UI state ----
+
+ void selectDeviceState(@Nullable State state) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mOrientationCombo.setData(state);
+
+ State nextState = mConfiguration.getNextDeviceState(state);
+ mOrientationCombo.setImage(getOrientationIcon(getOrientation(state),
+ nextState != state));
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ void selectTarget(IAndroidTarget target) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mTargetCombo.setData(target);
+ String label = getRenderingTargetLabel(target, true);
+ mTargetCombo.setText(label);
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ /**
+ * Selects a given {@link Device} in the device combo, if it is found.
+ * @param device the device to select
+ * @return true if the device was found.
+ */
+ boolean selectDevice(@Nullable Device device) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mDeviceCombo.setData(device);
+ if (device != null) {
+ mDeviceCombo.setText(getDeviceLabel(device, true));
+ } else {
+ mDeviceCombo.setText("Device");
+ }
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+
+ return false;
+ }
+
+ void selectActivity(@Nullable String fqcn) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ if (fqcn != null) {
+ mActivityCombo.setData(fqcn);
+ String label = getActivityLabel(fqcn, true);
+ mActivityCombo.setText(label);
+ } else {
+ mActivityCombo.setText("(Select)");
+ }
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ void selectTheme(@Nullable String theme) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ assert theme == null || theme.startsWith(STYLE_RESOURCE_PREFIX)
+ || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme;
+ mThemeCombo.setData(theme);
+ if (theme != null) {
+ mThemeCombo.setText(getThemeLabel(theme, true));
+ } else {
+ // FIXME eclipse claims this is dead code.
+ mThemeCombo.setText("(Set Theme)");
+ }
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ void selectLocale(@Nullable Locale locale) {
+ assert isUiThread();
+ try {
+ mDisableUpdates++;
+ mLocaleCombo.setData(locale);
+ String label = Strings.nullToEmpty(getLocaleLabel(this, locale, true));
+ mLocaleCombo.setText(label);
+
+ Image image = getFlagImage(locale);
+ mLocaleCombo.setImage(image);
+
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ @NonNull
+ Image getFlagImage(@Nullable Locale locale) {
+ if (locale != null) {
+ return locale.getFlagImage();
+ }
+
+ return FlagManager.getGlobeIcon();
+ }
+
+ private void selectConfiguration(FolderConfiguration fileConfig) {
+ /* For now, don't show any text in the configuration combo, use just an
+ icon. This has the advantage that the configuration contents don't
+ shift around, so you can for example click back and forth between
+ portrait and landscape without the icon moving under the mouse.
+ If this works well, remove this whole method post ADT 21.
+ assert isUiThread();
+ try {
+ String current = mEditedFile.getParent().getName();
+ if (current.equals(FD_RES_LAYOUT)) {
+ current = "default";
+ }
+
+ // Pretty things up a bit
+ //if (current == null || current.equals("default")) {
+ // current = "Default Configuration";
+ //}
+ mConfigCombo.setText(current);
+ resizeToolBar();
+ } finally {
+ mDisableUpdates--;
+ }
+ */
+ }
+
+ /**
+ * Finds a locale matching the config from a file.
+ *
+ * @param language the language qualifier or null if none is set.
+ * @param region the region qualifier or null if none is set.
+ * @return true if there was a change in the combobox as a result of
+ * applying the locale
+ */
+ private boolean setLocale(@Nullable Locale locale) {
+ boolean changed = !Objects.equal(mConfiguration.getLocale(), locale);
+ selectLocale(locale);
+
+ return changed;
+ }
+
+ // ---- Creating UI labels ----
+
+ /**
+ * Returns a suitable label to use to display the given activity
+ *
+ * @param fqcn the activity class to look up a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getActivityLabel(String fqcn, boolean brief) {
+ if (brief) {
+ String label = fqcn;
+ int packageIndex = label.lastIndexOf('.');
+ if (packageIndex != -1) {
+ label = label.substring(packageIndex + 1);
+ }
+ int innerClass = label.lastIndexOf('$');
+ if (innerClass != -1) {
+ label = label.substring(innerClass + 1);
+ }
+
+ // Also strip out the "Activity" or "Fragment" common suffix
+ // if this is a long name
+ if (label.endsWith("Activity") && label.length() > 8 + 12) { // 12 chars + 8 in suffix
+ label = label.substring(0, label.length() - 8);
+ } else if (label.endsWith("Fragment") && label.length() > 8 + 12) {
+ label = label.substring(0, label.length() - 8);
+ }
+
+ return label;
+ }
+
+ return fqcn;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given theme
+ *
+ * @param theme the theme to produce a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getThemeLabel(String theme, boolean brief) {
+ theme = ResourceHelper.styleToTheme(theme);
+
+ if (brief) {
+ int index = theme.lastIndexOf('.');
+ if (index < theme.length() - 1) {
+ return theme.substring(index + 1);
+ }
+ }
+ return theme;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given rendering target
+ *
+ * @param target the target to produce a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getRenderingTargetLabel(IAndroidTarget target, boolean brief) {
+ if (target == null) {
+ return "<null>";
+ }
+
+ AndroidVersion version = target.getVersion();
+
+ if (brief) {
+ if (target.isPlatform()) {
+ return Integer.toString(version.getApiLevel());
+ } else {
+ return target.getName() + ':' + Integer.toString(version.getApiLevel());
+ }
+ }
+
+ String label = String.format("API %1$d: %2$s",
+ version.getApiLevel(),
+ target.getShortClasspathName());
+
+ return label;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given device
+ *
+ * @param device the device to produce a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ public static String getDeviceLabel(@Nullable Device device, boolean brief) {
+ if (device == null) {
+ return "";
+ }
+ String name = device.getName();
+
+ if (brief) {
+ // Produce a really brief summary of the device name, suitable for
+ // use in the narrow space available in the toolbar for example
+ int nexus = name.indexOf("Nexus"); //$NON-NLS-1$
+ if (nexus != -1) {
+ int begin = name.indexOf('(');
+ if (begin != -1) {
+ begin++;
+ int end = name.indexOf(')', begin);
+ if (end != -1) {
+ return name.substring(begin, end).trim();
+ }
+ }
+ }
+ }
+
+ return name;
+ }
+
+ /**
+ * Returns a suitable label to use to display the given locale
+ *
+ * @param chooser the chooser, if known
+ * @param locale the locale to look up a label for
+ * @param brief if true, generate a brief label (suitable for a toolbar
+ * button), otherwise a fuller name (suitable for a menu item)
+ * @return the label
+ */
+ @Nullable
+ public static String getLocaleLabel(
+ @Nullable ConfigurationChooser chooser,
+ @Nullable Locale locale,
+ boolean brief) {
+ if (locale == null) {
+ return null;
+ }
+
+ if (!locale.hasLanguage()) {
+ if (brief) {
+ // Just use the icon
+ return "";
+ }
+
+ boolean hasLocale = false;
+ ResourceRepository projectRes = chooser != null ? chooser.mClient.getProjectResources()
+ : null;
+ if (projectRes != null) {
+ hasLocale = projectRes.getLanguages().size() > 0;
+ }
+
+ if (hasLocale) {
+ return "Other";
+ } else {
+ return "Any";
+ }
+ }
+
+ String languageCode = locale.qualifier.getLanguage();
+ String languageName = LocaleManager.getLanguageName(languageCode);
+
+ if (!locale.hasRegion()) {
+ // TODO: Make the region string use "Other" instead of "Any" if
+ // there is more than one region for a given language
+ //if (regions.size() > 0) {
+ // return String.format("%1$s / Other", language);
+ //} else {
+ // return String.format("%1$s / Any", language);
+ //}
+ if (!brief && languageName != null) {
+ return String.format("%1$s (%2$s)", languageName, languageCode);
+ } else {
+ return languageCode;
+ }
+ } else {
+ String regionCode = locale.qualifier.getRegion();
+ if (!brief && languageName != null) {
+ String regionName = LocaleManager.getRegionName(regionCode);
+ if (regionName != null) {
+ return String.format("%1$s (%2$s) in %3$s (%4$s)", languageName, languageCode,
+ regionName, regionCode);
+ }
+ return String.format("%1$s (%2$s) in %3$s", languageName, languageCode,
+ regionCode);
+ }
+ return String.format("%1$s / %2$s", languageCode, regionCode);
+ }
+ }
+
+ // ---- Implements DevicesChangedListener ----
+
+ @Override
+ public void onDevicesChanged() {
+ final Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ mDevices = sdk.getDeviceManager().getDevices(DeviceManager.ALL_DEVICES);
+ } else {
+ mDevices = new ArrayList<Device>();
+ }
+ }
+
+ // ---- Reacting to UI changes ----
+
+ /**
+ * Called when the selection of the device combo changes.
+ */
+ void onDeviceChange() {
+ // because changing the content of a combo triggers a change event, respect the
+ // mDisableUpdates flag
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ // Attempt to preserve the device state
+ String stateName = null;
+ Device prevDevice = mConfiguration.getDevice();
+ State prevState = mConfiguration.getDeviceState();
+ Device device = (Device) mDeviceCombo.getData();
+ if (prevDevice != null && prevState != null && device != null) {
+ // get the previous config, so that we can look for a close match
+ FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState);
+ if (oldConfig != null) {
+ stateName = ConfigurationMatcher.getClosestMatch(oldConfig, device.getAllStates());
+ }
+ }
+ mConfiguration.setDevice(device, true);
+ State newState = Configuration.getState(device, stateName);
+ mConfiguration.setDeviceState(newState, true);
+ selectDeviceState(newState);
+ mConfiguration.syncFolderConfig();
+
+ // Notify
+ IFile file = mEditedFile;
+ boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE);
+ if (!accepted) {
+ mConfiguration.setDevice(prevDevice, true);
+ mConfiguration.setDeviceState(prevState, true);
+ mConfiguration.syncFolderConfig();
+ selectDevice(prevDevice);
+ selectDeviceState(prevState);
+ return;
+ } else {
+ syncToVariations(CFG_DEVICE | CFG_DEVICE_STATE, file, mConfiguration, false, true);
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Synchronizes changes to the given attributes (indicated by the mask
+ * referencing the {@code CFG_} configuration attribute bit flags in
+ * {@link Configuration} to the layout variations of the given updated file.
+ *
+ * @param flags the attributes which were updated
+ * @param updatedFile the file which was updated
+ * @param base the base configuration to base the chooser off of
+ * @param includeSelf whether the updated file itself should be updated
+ * @param async whether the updates should be performed asynchronously
+ */
+ public void syncToVariations(
+ final int flags,
+ final @NonNull IFile updatedFile,
+ final @NonNull Configuration base,
+ final boolean includeSelf,
+ boolean async) {
+ if (async) {
+ getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ doSyncToVariations(flags, updatedFile, includeSelf, base);
+ }
+ });
+ } else {
+ doSyncToVariations(flags, updatedFile, includeSelf, base);
+ }
+ }
+
+ private void doSyncToVariations(int flags, IFile updatedFile, boolean includeSelf,
+ Configuration base) {
+ // Synchronize the given changes to other configurations as well
+ List<IFile> files = AdtUtils.getResourceVariations(updatedFile, includeSelf);
+ for (IFile file : files) {
+ Configuration configuration = Configuration.create(base, file);
+ configuration.setTheme(base.getTheme());
+ configuration.setActivity(base.getActivity());
+ Collection<IEditorPart> editors = AdtUtils.findEditorsFor(file, false);
+ boolean found = false;
+ for (IEditorPart editor : editors) {
+ if (editor instanceof CommonXmlEditor) {
+ CommonXmlDelegate delegate = ((CommonXmlEditor) editor).getDelegate();
+ if (delegate instanceof LayoutEditorDelegate) {
+ editor = ((LayoutEditorDelegate) delegate).getGraphicalEditor();
+ }
+ }
+ if (editor instanceof GraphicalEditorPart) {
+ ConfigurationChooser chooser =
+ ((GraphicalEditorPart) editor).getConfigurationChooser();
+ chooser.setConfiguration(configuration);
+ found = true;
+ }
+ }
+ if (!found) {
+ // Just update the file persistence
+ String description = configuration.toPersistentString();
+ ConfigurationDescription.setDescription(file, description);
+ }
+ }
+ }
+
+ /**
+ * Called when the device config selection changes.
+ */
+ void onDeviceConfigChange() {
+ // because changing the content of a combo triggers a change event, respect the
+ // mDisableUpdates flag
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ State prev = mConfiguration.getDeviceState();
+ State state = (State) mOrientationCombo.getData();
+ mConfiguration.setDeviceState(state, false);
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE);
+ if (!accepted) {
+ mConfiguration.setDeviceState(prev, false);
+ selectDeviceState(prev);
+ return;
+ }
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Call back for language combo selection
+ */
+ void onLocaleChange() {
+ // because mLocaleList triggers onLocaleChange at each modification, the filling
+ // of the combo with data will trigger notifications, and we don't want that.
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ Locale prev = mConfiguration.getLocale();
+ Locale locale = (Locale) mLocaleCombo.getData();
+ if (locale == null) {
+ locale = Locale.ANY;
+ }
+ mConfiguration.setLocale(locale, false);
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(CFG_LOCALE);
+ if (!accepted) {
+ mConfiguration.setLocale(prev, false);
+ selectLocale(prev);
+ }
+ }
+
+ // Store locale project-wide setting
+ mConfiguration.saveRenderState();
+ }
+
+
+ void onThemeChange() {
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ String prev = mConfiguration.getTheme();
+ mConfiguration.setTheme((String) mThemeCombo.getData());
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(CFG_THEME);
+ if (!accepted) {
+ mConfiguration.setTheme(prev);
+ selectTheme(prev);
+ return;
+ } else {
+ syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, mEditedFile, mConfiguration,
+ false, true);
+ }
+ }
+
+ saveConstraints();
+ }
+
+ void notifyFolderConfigChanged() {
+ if (mDisableUpdates > 0 || mClient == null) {
+ return;
+ }
+
+ if (mClient.changed(CFG_FOLDER)) {
+ saveConstraints();
+ }
+ }
+
+ void onSelectActivity() {
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ String activity = (String) mActivityCombo.getData();
+ mConfiguration.setActivity(activity);
+
+ if (activity == null) {
+ return;
+ }
+
+ // See if there is a default theme assigned to this activity, and if so, use it
+ ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject());
+ String preferred = null;
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ preferred = attributes.getTheme();
+ }
+ if (preferred != null && !Objects.equal(preferred, mConfiguration.getTheme())) {
+ // Yes, switch to it
+ selectTheme(preferred);
+ onThemeChange();
+ }
+
+ // Persist in XML
+ if (mClient != null) {
+ mClient.setActivity(activity);
+ }
+
+ saveConstraints();
+ }
+
+ /**
+ * Call back for api level combo selection
+ */
+ void onRenderingTargetChange() {
+ // because mApiCombo triggers onApiLevelChange at each modification, the filling
+ // of the combo with data will trigger notifications, and we don't want that.
+ if (mDisableUpdates > 0) {
+ return;
+ }
+
+ IAndroidTarget prevTarget = mConfiguration.getTarget();
+ String prevTheme = mConfiguration.getTheme();
+
+ int changeFlags = 0;
+
+ // tell the listener a new rendering target is being set. Need to do this before updating
+ // mRenderingTarget.
+ if (prevTarget != null) {
+ changeFlags |= CFG_TARGET;
+ mClient.aboutToChange(changeFlags);
+ }
+
+ IAndroidTarget target = (IAndroidTarget) mTargetCombo.getData();
+ mConfiguration.setTarget(target, true);
+
+ // force a theme update to reflect the new rendering target.
+ // This must be done after computeCurrentConfig since it'll depend on the currentConfig
+ // to figure out the theme list.
+ String oldTheme = mConfiguration.getTheme();
+ updateThemes();
+ // updateThemes may change the theme (based on theme availability in the new rendering
+ // target) so mark theme change if necessary
+ if (!Objects.equal(oldTheme, mConfiguration.getTheme())) {
+ changeFlags |= CFG_THEME;
+ }
+
+ if (target != null) {
+ changeFlags |= CFG_TARGET;
+ changeFlags |= CFG_FOLDER; // In case we added a -vNN qualifier
+ }
+
+ // Store project-wide render-target setting
+ mConfiguration.saveRenderState();
+
+ mConfiguration.syncFolderConfig();
+
+ if (mClient != null) {
+ boolean accepted = mClient.changed(changeFlags);
+ if (!accepted) {
+ mConfiguration.setTarget(prevTarget, true);
+ mConfiguration.setTheme(prevTheme);
+ mConfiguration.syncFolderConfig();
+ selectTheme(prevTheme);
+ selectTarget(prevTarget);
+ }
+ }
+ }
+
+ /**
+ * Syncs this configuration to the project wide locale and render target settings. The
+ * locale may ignore the project-wide setting if it is a locale-specific
+ * configuration.
+ *
+ * @return true if one or both of the toggles were changed, false if there were no
+ * changes
+ */
+ public boolean syncRenderState() {
+ if (mConfiguration.getEditedConfig() == null) {
+ // Startup; ignore
+ return false;
+ }
+
+ boolean renderTargetChanged = false;
+
+ // When a page is re-activated, force the toggles to reflect the current project
+ // state
+
+ Pair<Locale, IAndroidTarget> pair = Configuration.loadRenderState(this);
+
+ int changeFlags = 0;
+ // Only sync the locale if this layout is not already a locale-specific layout!
+ if (pair != null && !mConfiguration.isLocaleSpecificLayout()) {
+ Locale locale = pair.getFirst();
+ if (locale != null) {
+ boolean localeChanged = setLocale(locale);
+ if (localeChanged) {
+ changeFlags |= CFG_LOCALE;
+ }
+ } else {
+ locale = Locale.ANY;
+ }
+ mConfiguration.setLocale(locale, true);
+ }
+
+ // Sync render target
+ IAndroidTarget configurationTarget = mConfiguration.getTarget();
+ IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget;
+ if (target != null && configurationTarget != target) {
+ if (mClient != null && configurationTarget != null) {
+ changeFlags |= CFG_TARGET;
+ mClient.aboutToChange(changeFlags);
+ }
+
+ mConfiguration.setTarget(target, true);
+ selectTarget(target);
+ renderTargetChanged = true;
+ }
+
+ // Neither locale nor render target changed: nothing to do
+ if (changeFlags == 0) {
+ return false;
+ }
+
+ // Update the locale and/or the render target. This code contains a logical
+ // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined
+ // such that we don't duplicate work.
+
+ // Compute the new configuration; we want to do this both for locale changes
+ // and for render targets.
+ mConfiguration.syncFolderConfig();
+ changeFlags |= CFG_FOLDER; // in case we added/remove a -v<NN> qualifier
+
+ if (renderTargetChanged) {
+ // force a theme update to reflect the new rendering target.
+ // This must be done after computeCurrentConfig since it'll depend on the currentConfig
+ // to figure out the theme list.
+ updateThemes();
+ }
+
+ if (mClient != null) {
+ mClient.changed(changeFlags);
+ }
+
+ return true;
+ }
+
+ // ---- Populate data structures with themes, locales, etc ----
+
+ /**
+ * Updates the internal list of themes.
+ */
+ private void updateThemes() {
+ if (mClient == null) {
+ return; // can't do anything without it.
+ }
+
+ ResourceRepository frameworkRes = mClient.getFrameworkResources(
+ mConfiguration.getTarget());
+
+ mDisableUpdates++;
+
+ try {
+ if (mEditedFile != null) {
+ String theme = mConfiguration.getTheme();
+ if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) {
+ mConfiguration.setTheme(null);
+ mConfiguration.computePreferredTheme();
+ }
+ assert mConfiguration.getTheme() != null;
+ }
+
+ mThemeList.clear();
+
+ ArrayList<String> themes = new ArrayList<String>();
+ ResourceRepository projectRes = mClient.getProjectResources();
+ // in cases where the opened file is not linked to a project, this could be null.
+ if (projectRes != null) {
+ // get the configured resources for the project
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
+ mClient.getConfiguredProjectResources();
+
+ if (configuredProjectRes != null) {
+ // get the styles.
+ Map<String, ResourceValue> styleMap = configuredProjectRes.get(
+ ResourceType.STYLE);
+
+ if (styleMap != null) {
+ // collect the themes out of all the styles, ie styles that extend,
+ // directly or indirectly a platform theme.
+ for (ResourceValue value : styleMap.values()) {
+ if (isTheme(value, styleMap, null)) {
+ String theme = value.getName();
+ themes.add(theme);
+ }
+ }
+
+ Collections.sort(themes);
+
+ for (String theme : themes) {
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ mThemeList.add(theme);
+ }
+ }
+ }
+ themes.clear();
+ }
+
+ // get the themes, and languages from the Framework.
+ if (frameworkRes != null) {
+ // get the configured resources for the framework
+ Map<ResourceType, Map<String, ResourceValue>> frameworResources =
+ frameworkRes.getConfiguredResources(mConfiguration.getFullConfig());
+
+ if (frameworResources != null) {
+ // get the styles.
+ Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE);
+
+ // collect the themes out of all the styles.
+ for (ResourceValue value : styles.values()) {
+ String name = value.getName();
+ if (name.startsWith("Theme.") || name.equals("Theme")) { //$NON-NLS-1$ //$NON-NLS-2$
+ themes.add(value.getName());
+ }
+ }
+
+ // sort them and add them to the combo
+ Collections.sort(themes);
+
+ for (String theme : themes) {
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ }
+ mThemeList.add(theme);
+ }
+
+ themes.clear();
+ }
+ }
+
+ // Migration: In the past we didn't store the style prefix in the settings;
+ // this meant we might lose track of whether the theme is a project style
+ // or a framework style. For now we need to migrate. Search through the
+ // theme list until we have a match
+ String theme = mConfiguration.getTheme();
+ if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
+ String projectStyle = STYLE_RESOURCE_PREFIX + theme;
+ String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ for (String t : mThemeList) {
+ if (t.equals(projectStyle)) {
+ mConfiguration.setTheme(projectStyle);
+ break;
+ } else if (t.equals(frameworkStyle)) {
+ mConfiguration.setTheme(frameworkStyle);
+ break;
+ }
+ }
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ // Arbitrary guess
+ if (theme.startsWith("Theme.")) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ } else {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+ }
+
+ // TODO: Handle the case where you have a theme persisted that isn't available??
+ // We could look up mConfiguration.theme and make sure it appears in the list! And if
+ // not, picking one.
+ selectTheme(mConfiguration.getTheme());
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ private void updateActivity() {
+ if (mEditedFile != null) {
+ String preferred = getPreferredActivity(mEditedFile);
+ selectActivity(preferred);
+ }
+ }
+
+ /**
+ * Updates the locale combo.
+ * This must be called from the UI thread.
+ */
+ public void updateLocales() {
+ if (mClient == null) {
+ return; // can't do anything w/o it.
+ }
+
+ mDisableUpdates++;
+
+ try {
+ mLocaleList.clear();
+
+ SortedSet<String> languages = null;
+
+ // get the languages from the project.
+ ResourceRepository projectRes = mClient.getProjectResources();
+
+ // in cases where the opened file is not linked to a project, this could be null.
+ if (projectRes != null) {
+ // now get the languages from the project.
+ languages = projectRes.getLanguages();
+
+ for (String language : languages) {
+ // find the matching regions and add them
+ SortedSet<String> regions = projectRes.getRegions(language);
+ for (String region : regions) {
+ LocaleQualifier locale = LocaleQualifier.getQualifier(language + "-r" + region);
+ if (locale != null) {
+ mLocaleList.add(Locale.create(locale));
+ }
+ }
+
+ // now the entry for the other regions the language alone
+ // create a region qualifier that will never be matched by qualified resources.
+ LocaleQualifier locale = new LocaleQualifier(language);
+ mLocaleList.add(Locale.create(locale));
+ }
+ }
+
+ // create language/region qualifier that will never be matched by qualified resources.
+ mLocaleList.add(Locale.ANY);
+
+ Locale locale = mConfiguration.getLocale();
+ setLocale(locale);
+ } finally {
+ mDisableUpdates--;
+ }
+ }
+
+ @Nullable
+ private String getPreferredActivity(@NonNull IFile file) {
+ // Store/restore the activity context in the config state to help with
+ // performance if for some reason we can't write it into the XML file and to
+ // avoid having to open the model below
+ if (mConfiguration.getActivity() != null) {
+ return mConfiguration.getActivity();
+ }
+
+ IProject project = file.getProject();
+
+ // Look up from XML file
+ Document document = DomUtilities.getDocument(file);
+ if (document != null) {
+ Element element = document.getDocumentElement();
+ if (element != null) {
+ String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT);
+ if (activity != null && !activity.isEmpty()) {
+ if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$
+ ManifestInfo manifest = ManifestInfo.get(project);
+ String pkg = manifest.getPackage();
+ if (!pkg.isEmpty()) {
+ if (activity.startsWith(".")) { //$NON-NLS-1$
+ activity = pkg + activity;
+ } else {
+ activity = activity + '.' + pkg;
+ }
+ }
+ }
+
+ mConfiguration.setActivity(activity);
+ saveConstraints();
+ return activity;
+ }
+ }
+ }
+
+ // No, not available there: try to infer it from the code index
+ String includedIn = null;
+ Reference includedWithin = mClient.getIncludedWithin();
+ if (mClient != null && includedWithin != null) {
+ includedIn = includedWithin.getName();
+ }
+
+ ManifestInfo manifest = ManifestInfo.get(project);
+ String pkg = manifest.getPackage();
+ String layoutName = ResourceHelper.getLayoutName(mEditedFile);
+
+ // If we are rendering a layout in included context, pick the theme
+ // from the outer layout instead
+ if (includedIn != null) {
+ layoutName = includedIn;
+ }
+
+ String activity = ManifestInfo.guessActivity(project, layoutName, pkg);
+
+ if (activity == null) {
+ List<String> activities = ManifestInfo.getProjectActivities(project);
+ if (activities.size() == 1) {
+ activity = activities.get(0);
+ }
+ }
+
+ if (activity != null) {
+ mConfiguration.setActivity(activity);
+ saveConstraints();
+ return activity;
+ }
+
+ // TODO: Do anything else, such as pick the first activity found?
+ // Or just leave some default label instead?
+ // Also, figure out what to store in the mState so I don't keep trying
+
+ return null;
+ }
+
+ /**
+ * Returns whether the given <var>style</var> is a theme.
+ * This is done by making sure the parent is a theme.
+ * @param value the style to check
+ * @param styleMap the map of styles for the current project. Key is the style name.
+ * @param seen the map of styles we have already processed (or null if not yet
+ * initialized). Only the keys are significant (since there is no IdentityHashSet).
+ * @return True if the given <var>style</var> is a theme.
+ */
+ private static boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap,
+ IdentityHashMap<ResourceValue, Boolean> seen) {
+ if (value instanceof StyleResourceValue) {
+ StyleResourceValue style = (StyleResourceValue)value;
+
+ boolean frameworkStyle = false;
+ String parentStyle = style.getParentStyle();
+ if (parentStyle == null) {
+ // if there is no specified parent style we look an implied one.
+ // For instance 'Theme.light' is implied child style of 'Theme',
+ // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
+ String name = style.getName();
+ int index = name.lastIndexOf('.');
+ if (index != -1) {
+ parentStyle = name.substring(0, index);
+ }
+ } else {
+ // remove the useless @ if it's there
+ if (parentStyle.startsWith("@")) {
+ parentStyle = parentStyle.substring(1);
+ }
+
+ // check for framework identifier.
+ if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) {
+ frameworkStyle = true;
+ parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length());
+ }
+
+ // at this point we could have the format style/<name>. we want only the name
+ if (parentStyle.startsWith("style/")) {
+ parentStyle = parentStyle.substring("style/".length());
+ }
+ }
+
+ if (parentStyle != null) {
+ if (frameworkStyle) {
+ // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
+ return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
+ } else {
+ // if it's a project style, we check this is a theme.
+ ResourceValue parentValue = styleMap.get(parentStyle);
+
+ // also prevent stack overflow in case the dev mistakenly declared
+ // the parent of the style as the style itself.
+ if (parentValue != null && !parentValue.equals(value)) {
+ if (seen == null) {
+ seen = new IdentityHashMap<ResourceValue, Boolean>();
+ seen.put(value, Boolean.TRUE);
+ } else if (seen.containsKey(parentValue)) {
+ return false;
+ }
+ seen.put(parentValue, Boolean.TRUE);
+ return isTheme(parentValue, styleMap, seen);
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this configuration chooser represents the best match for
+ * the given file
+ *
+ * @param file the file to test
+ * @param config the config to test
+ * @return true if the given config is the best match for the given file
+ */
+ public boolean isBestMatchFor(IFile file, FolderConfiguration config) {
+ ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
+ ResourceType.LAYOUT, config);
+ if (match != null) {
+ return match.getFile().equals(mEditedFile);
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java
new file mode 100644
index 000000000..3df2feda3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+
+import java.util.Map;
+
+/**
+ * Interface implemented by clients who embed a {@link ConfigurationChooser}.
+ */
+public interface ConfigurationClient {
+ /**
+ * The configuration is about to be changed.
+ *
+ * @param flags details about what changed; consult the {@code CFG_} flags
+ * in {@link Configuration} such as
+ * {@link Configuration#CFG_DEVICE},
+ * {@link Configuration#CFG_LOCALE}, etc.
+ */
+ void aboutToChange(int flags);
+
+ /**
+ * The configuration has changed. If the client returns false, it means that
+ * the change was rejected. This typically means that changing the
+ * configuration in this particular way makes a configuration which has a
+ * better file match than the current client's file, so it will open that
+ * file to edit the new configuration -- and the current configuration
+ * should go back to editing the state prior to this change.
+ *
+ * @param flags details about what changed; consult the {@code CFG_} flags
+ * such as {@link Configuration#CFG_DEVICE},
+ * {@link Configuration#CFG_LOCALE}, etc.
+ * @return true if the change was accepted, false if it was rejected.
+ */
+ boolean changed(int flags);
+
+ /**
+ * Compute the project resources
+ *
+ * @return the project resources as a {@link ResourceRepository}
+ */
+ @Nullable
+ ResourceRepository getProjectResources();
+
+ /**
+ * Compute the framework resources
+ *
+ * @return the project resources as a {@link ResourceRepository}
+ */
+ @Nullable
+ ResourceRepository getFrameworkResources();
+
+ /**
+ * Compute the framework resources for the given Android API target
+ *
+ * @param target the target to look up framework resources for
+ * @return the project resources as a {@link ResourceRepository}
+ */
+ @Nullable
+ ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target);
+
+ /**
+ * Returns the configured project resources for the current file and
+ * configuration
+ *
+ * @return resource type maps to names to resource values
+ */
+ @NonNull
+ Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources();
+
+ /**
+ * Returns the configured framework resources for the current file and
+ * configuration
+ *
+ * @return resource type maps to names to resource values
+ */
+ @NonNull
+ Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources();
+
+ /**
+ * If the current layout is an included layout rendered within an outer layout,
+ * returns the outer layout.
+ *
+ * @return the outer including layout, or null
+ */
+ @Nullable
+ Reference getIncludedWithin();
+
+ /**
+ * Called when the "Create" button is clicked.
+ */
+ void createConfigFile();
+
+ /**
+ * Called when an associated activity is picked
+ *
+ * @param fqcn the fully qualified class name for the associated activity context
+ */
+ void setActivity(@NonNull String fqcn);
+
+ /**
+ * Returns the associated layout canvas, if any
+ *
+ * @return the canvas, if any
+ */
+ @Nullable
+ LayoutCanvas getCanvas();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java
new file mode 100644
index 000000000..956ac1839
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_THEME;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.NightMode;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ScreenSize;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.google.common.base.Splitter;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.QualifiedName;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.List;
+
+/** A description of a configuration, used for persistence */
+public class ConfigurationDescription {
+ private static final String TAG_PREVIEWS = "previews"; //$NON-NLS-1$
+ private static final String TAG_PREVIEW = "preview"; //$NON-NLS-1$
+ private static final String ATTR_TARGET = "target"; //$NON-NLS-1$
+ private static final String ATTR_CONFIG = "config"; //$NON-NLS-1$
+ private static final String ATTR_LOCALE = "locale"; //$NON-NLS-1$
+ private static final String ATTR_ACTIVITY = "activity"; //$NON-NLS-1$
+ private static final String ATTR_DEVICE = "device"; //$NON-NLS-1$
+ private static final String ATTR_STATE = "devicestate"; //$NON-NLS-1$
+ private static final String ATTR_UIMODE = "ui"; //$NON-NLS-1$
+ private static final String ATTR_NIGHTMODE = "night"; //$NON-NLS-1$
+ private final static String SEP_LOCALE = "-"; //$NON-NLS-1$
+
+ /**
+ * Settings name for file-specific configuration preferences, such as which theme or
+ * device to render the current layout with
+ */
+ public final static QualifiedName NAME_CONFIG_STATE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$
+
+ /** The project corresponding to this configuration's description */
+ public final IProject project;
+
+ /** The display name */
+ public String displayName;
+
+ /** The theme */
+ public String theme;
+
+ /** The target */
+ public IAndroidTarget target;
+
+ /** The display name */
+ public FolderConfiguration folder;
+
+ /** The locale */
+ public Locale locale = Locale.ANY;
+
+ /** The device */
+ public Device device;
+
+ /** The device state */
+ public State state;
+
+ /** The activity */
+ public String activity;
+
+ /** UI mode */
+ @NonNull
+ public UiMode uiMode = UiMode.NORMAL;
+
+ /** Night mode */
+ @NonNull
+ public NightMode nightMode = NightMode.NOTNIGHT;
+
+ private ConfigurationDescription(@Nullable IProject project) {
+ this.project = project;
+ }
+
+ /**
+ * Returns the persistent configuration description from the given file
+ *
+ * @param file the file to look up a description from
+ * @return the description or null if never written
+ */
+ @Nullable
+ public static String getDescription(@NonNull IFile file) {
+ return AdtPlugin.getFileProperty(file, NAME_CONFIG_STATE);
+ }
+
+ /**
+ * Sets the persistent configuration description data for the given file
+ *
+ * @param file the file to associate the description with
+ * @param description the description
+ */
+ public static void setDescription(@NonNull IFile file, @NonNull String description) {
+ AdtPlugin.setFileProperty(file, NAME_CONFIG_STATE, description);
+ }
+
+ /**
+ * Creates a description from a given configuration
+ *
+ * @param project the project for this configuration's description
+ * @param configuration the configuration to describe
+ * @return a new configuration
+ */
+ public static ConfigurationDescription fromConfiguration(
+ @Nullable IProject project,
+ @NonNull Configuration configuration) {
+ ConfigurationDescription description = new ConfigurationDescription(project);
+ description.displayName = configuration.getDisplayName();
+ description.theme = configuration.getTheme();
+ description.target = configuration.getTarget();
+ description.folder = new FolderConfiguration();
+ description.folder.set(configuration.getFullConfig());
+ description.locale = configuration.getLocale();
+ description.device = configuration.getDevice();
+ description.state = configuration.getDeviceState();
+ description.activity = configuration.getActivity();
+ return description;
+ }
+
+ /**
+ * Initializes a string previously created with
+ * {@link #toXml(Document)}
+ *
+ * @param project the project for this configuration's description
+ * @param element the element to read back from
+ * @param deviceList list of available devices
+ * @return true if the configuration was initialized
+ */
+ @Nullable
+ public static ConfigurationDescription fromXml(
+ @Nullable IProject project,
+ @NonNull Element element,
+ @NonNull Collection<Device> deviceList) {
+ ConfigurationDescription description = new ConfigurationDescription(project);
+
+ if (!TAG_PREVIEW.equals(element.getTagName())) {
+ return null;
+ }
+
+ String displayName = element.getAttribute(ATTR_NAME);
+ if (!displayName.isEmpty()) {
+ description.displayName = displayName;
+ }
+
+ String config = element.getAttribute(ATTR_CONFIG);
+ Iterable<String> segments = Splitter.on('-').split(config);
+ description.folder = FolderConfiguration.getConfig(segments);
+
+ String theme = element.getAttribute(ATTR_THEME);
+ if (!theme.isEmpty()) {
+ description.theme = theme;
+ }
+
+ String targetId = element.getAttribute(ATTR_TARGET);
+ if (!targetId.isEmpty()) {
+ IAndroidTarget target = Configuration.stringToTarget(targetId);
+ description.target = target;
+ }
+
+ String localeString = element.getAttribute(ATTR_LOCALE);
+ if (!localeString.isEmpty()) {
+ // Load locale. Note that this can get overwritten by the
+ // project-wide settings read below.
+ String locales[] = localeString.split(SEP_LOCALE);
+ if (locales[0].length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) {
+ String language = locales[0];
+ if (locales.length >= 2 && locales[1].length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(locales[1])) {
+ description.locale = Locale.create(LocaleQualifier.getQualifier(language + "-r" + locales[1]));
+ } else {
+ description.locale = Locale.create(new LocaleQualifier(language));
+ }
+ } else {
+ description.locale = Locale.ANY;
+ }
+
+
+ }
+
+ String activity = element.getAttribute(ATTR_ACTIVITY);
+ if (activity.isEmpty()) {
+ activity = null;
+ }
+
+ String deviceString = element.getAttribute(ATTR_DEVICE);
+ if (!deviceString.isEmpty()) {
+ for (Device d : deviceList) {
+ if (d.getName().equals(deviceString)) {
+ description.device = d;
+ String stateName = element.getAttribute(ATTR_STATE);
+ if (stateName.isEmpty() || stateName.equals("null")) {
+ description.state = Configuration.getState(d, stateName);
+ } else if (d.getAllStates().size() > 0) {
+ description.state = d.getAllStates().get(0);
+ }
+ break;
+ }
+ }
+ }
+
+ String uiModeString = element.getAttribute(ATTR_UIMODE);
+ if (!uiModeString.isEmpty()) {
+ description.uiMode = UiMode.getEnum(uiModeString);
+ if (description.uiMode == null) {
+ description.uiMode = UiMode.NORMAL;
+ }
+ }
+
+ String nightModeString = element.getAttribute(ATTR_NIGHTMODE);
+ if (!nightModeString.isEmpty()) {
+ description.nightMode = NightMode.getEnum(nightModeString);
+ if (description.nightMode == null) {
+ description.nightMode = NightMode.NOTNIGHT;
+ }
+ }
+
+
+ // Should I really be storing the FULL configuration? Might be trouble if
+ // you bring a different device
+
+ return description;
+ }
+
+ /**
+ * Write this description into the given document as a new element.
+ *
+ * @param document the document to add the description to
+ * @return the newly inserted element
+ */
+ @NonNull
+ public Element toXml(Document document) {
+ Element element = document.createElement(TAG_PREVIEW);
+
+ element.setAttribute(ATTR_NAME, displayName);
+ FolderConfiguration fullConfig = folder;
+ String folderName = fullConfig.getFolderName(ResourceFolderType.LAYOUT);
+ element.setAttribute(ATTR_CONFIG, folderName);
+ if (theme != null) {
+ element.setAttribute(ATTR_THEME, theme);
+ }
+ if (target != null) {
+ element.setAttribute(ATTR_TARGET, Configuration.targetToString(target));
+ }
+
+ if (locale != null && (locale.hasLanguage() || locale.hasRegion())) {
+ String value;
+ if (locale.hasRegion()) {
+ value = locale.qualifier.getLanguage() + SEP_LOCALE + locale.qualifier.getRegion();
+ } else {
+ value = locale.qualifier.getLanguage();
+ }
+ element.setAttribute(ATTR_LOCALE, value);
+ }
+
+ if (device != null) {
+ element.setAttribute(ATTR_DEVICE, device.getName());
+ if (state != null) {
+ element.setAttribute(ATTR_STATE, state.getName());
+ }
+ }
+
+ if (activity != null) {
+ element.setAttribute(ATTR_ACTIVITY, activity);
+ }
+
+ if (uiMode != null && uiMode != UiMode.NORMAL) {
+ element.setAttribute(ATTR_UIMODE, uiMode.getResourceValue());
+ }
+
+ if (nightMode != null && nightMode != NightMode.NOTNIGHT) {
+ element.setAttribute(ATTR_NIGHTMODE, nightMode.getResourceValue());
+ }
+
+ Element parent = document.getDocumentElement();
+ if (parent == null) {
+ parent = document.createElement(TAG_PREVIEWS);
+ document.appendChild(parent);
+ }
+ parent.appendChild(element);
+
+ return element;
+ }
+
+ /** Returns the preferred theme, or null */
+ @Nullable
+ String computePreferredTheme() {
+ if (project == null) {
+ return "Theme";
+ }
+ ManifestInfo manifest = ManifestInfo.get(project);
+
+ // Look up the screen size for the current state
+ ScreenSize screenSize = null;
+ if (device != null) {
+ List<State> states = device.getAllStates();
+ for (State s : states) {
+ FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s);
+ if (folderConfig != null) {
+ ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier();
+ screenSize = qualifier.getValue();
+ break;
+ }
+ }
+ }
+
+ // Look up the default/fallback theme to use for this project (which
+ // depends on the screen size when no particular theme is specified
+ // in the manifest)
+ String defaultTheme = manifest.getDefaultTheme(target, screenSize);
+
+ String preferred = defaultTheme;
+ if (theme == null) {
+ // If we are rendering a layout in included context, pick the theme
+ // from the outer layout instead
+
+ if (activity != null) {
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ preferred = attributes.getTheme();
+ }
+ }
+ if (preferred == null) {
+ preferred = defaultTheme;
+ }
+ theme = preferred;
+ }
+
+ return preferred;
+ }
+
+ private void checkThemePrefix() {
+ if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (theme.isEmpty()) {
+ computePreferredTheme();
+ return;
+ }
+
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AndroidTargetData data = sdk.getTargetData(target);
+
+ if (data != null) {
+ ResourceRepository resources = data.getFrameworkResources();
+ if (resources != null
+ && resources.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ return;
+ }
+ }
+ }
+ }
+
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java
new file mode 100644
index 000000000..9724d4015
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java
@@ -0,0 +1,843 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.NightModeQualifier;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.common.resources.configuration.UiModeQualifier;
+import com.android.ide.common.resources.configuration.VersionQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.resources.Density;
+import com.android.resources.NightMode;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+import com.android.resources.ScreenSize;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.android.sdklib.repository.PkgProps;
+import com.android.utils.Pair;
+import com.android.utils.SparseIntArray;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.ui.IEditorPart;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Produces matches for configurations
+ * <p>
+ * See algorithm described here:
+ * http://developer.android.com/guide/topics/resources/providing-resources.html
+ */
+public class ConfigurationMatcher {
+ private static final boolean PREFER_RECENT_RENDER_TARGETS = true;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final Configuration mConfiguration;
+ private final IFile mEditedFile;
+ private final ProjectResources mResources;
+ private final boolean mUpdateUi;
+
+ ConfigurationMatcher(ConfigurationChooser chooser) {
+ this(chooser, chooser.getConfiguration(), chooser.getEditedFile(),
+ chooser.getResources(), true);
+ }
+
+ ConfigurationMatcher(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull Configuration configuration,
+ @Nullable IFile editedFile,
+ @Nullable ProjectResources resources,
+ boolean updateUi) {
+ mConfigChooser = chooser;
+ mConfiguration = configuration;
+ mEditedFile = editedFile;
+ mResources = resources;
+ mUpdateUi = updateUi;
+ }
+
+ // ---- Finding matching configurations ----
+
+ private static class ConfigBundle {
+ private final FolderConfiguration config;
+ private int localeIndex;
+ private int dockModeIndex;
+ private int nightModeIndex;
+
+ private ConfigBundle() {
+ config = new FolderConfiguration();
+ }
+
+ private ConfigBundle(ConfigBundle bundle) {
+ config = new FolderConfiguration();
+ config.set(bundle.config);
+ localeIndex = bundle.localeIndex;
+ dockModeIndex = bundle.dockModeIndex;
+ nightModeIndex = bundle.nightModeIndex;
+ }
+ }
+
+ private static class ConfigMatch {
+ final FolderConfiguration testConfig;
+ final Device device;
+ final State state;
+ final ConfigBundle bundle;
+
+ public ConfigMatch(@NonNull FolderConfiguration testConfig, @NonNull Device device,
+ @NonNull State state, @NonNull ConfigBundle bundle) {
+ this.testConfig = testConfig;
+ this.device = device;
+ this.state = state;
+ this.bundle = bundle;
+ }
+
+ @Override
+ public String toString() {
+ return device.getName() + " - " + state.getName();
+ }
+ }
+
+ /**
+ * Checks whether the current edited file is the best match for a given config.
+ * <p>
+ * This tests against other versions of the same layout in the project.
+ * <p>
+ * The given config must be compatible with the current edited file.
+ * @param config the config to test.
+ * @return true if the current edited file is the best match in the project for the
+ * given config.
+ */
+ public boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
+ ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
+ ResourceType.LAYOUT, config);
+
+ if (match != null) {
+ return match.getFile().equals(mEditedFile);
+ } else {
+ // if we stop here that means the current file is not even a match!
+ AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
+ }
+
+ return false;
+ }
+
+ /**
+ * Adapts the current device/config selection so that it's compatible with
+ * the configuration.
+ * <p>
+ * If the current selection is compatible, nothing is changed.
+ * <p>
+ * If it's not compatible, configs from the current devices are tested.
+ * <p>
+ * If none are compatible, it reverts to
+ * {@link #findAndSetCompatibleConfig(boolean)}
+ */
+ void adaptConfigSelection(boolean needBestMatch) {
+ // check the device config (ie sans locale)
+ boolean needConfigChange = true; // if still true, we need to find another config.
+ boolean currentConfigIsCompatible = false;
+ State selectedState = mConfiguration.getDeviceState();
+ FolderConfiguration editedConfig = mConfiguration.getEditedConfig();
+ if (selectedState != null) {
+ FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState);
+ if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) {
+ currentConfigIsCompatible = true; // current config is compatible
+ if (!needBestMatch || isCurrentFileBestMatchFor(currentConfig)) {
+ needConfigChange = false;
+ }
+ }
+ }
+
+ if (needConfigChange) {
+ List<Locale> localeList = mConfigChooser.getLocaleList();
+
+ // if the current state/locale isn't a correct match, then
+ // look for another state/locale in the same device.
+ FolderConfiguration testConfig = new FolderConfiguration();
+
+ // first look in the current device.
+ State matchState = null;
+ int localeIndex = -1;
+ Device device = mConfiguration.getDevice();
+ if (device != null) {
+ mainloop: for (State state : device.getAllStates()) {
+ testConfig.set(DeviceConfigHelper.getFolderConfig(state));
+
+ // loop on the locales.
+ for (int i = 0 ; i < localeList.size() ; i++) {
+ Locale locale = localeList.get(i);
+
+ // update the test config with the locale qualifiers
+ testConfig.setLocaleQualifier(locale.qualifier);
+
+
+ if (editedConfig.isMatchFor(testConfig) &&
+ isCurrentFileBestMatchFor(testConfig)) {
+ matchState = state;
+ localeIndex = i;
+ break mainloop;
+ }
+ }
+ }
+ }
+
+ if (matchState != null) {
+ mConfiguration.setDeviceState(matchState, true);
+ Locale locale = localeList.get(localeIndex);
+ mConfiguration.setLocale(locale, true);
+ if (mUpdateUi) {
+ mConfigChooser.selectDeviceState(matchState);
+ mConfigChooser.selectLocale(locale);
+ }
+ mConfiguration.syncFolderConfig();
+ } else {
+ // no match in current device with any state/locale
+ // attempt to find another device that can display this
+ // particular state.
+ findAndSetCompatibleConfig(currentConfigIsCompatible);
+ }
+ }
+ }
+
+ /**
+ * Finds a device/config that can display a configuration.
+ * <p>
+ * Once found the device and config combos are set to the config.
+ * <p>
+ * If there is no compatible configuration, a custom one is created.
+ *
+ * @param favorCurrentConfig if true, and no best match is found, don't
+ * change the current config. This must only be true if the
+ * current config is compatible.
+ */
+ void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
+ List<Locale> localeList = mConfigChooser.getLocaleList();
+ Collection<Device> devices = mConfigChooser.getDevices();
+ FolderConfiguration editedConfig = mConfiguration.getEditedConfig();
+ FolderConfiguration currentConfig = mConfiguration.getFullConfig();
+
+ // list of compatible device/state/locale
+ List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
+
+ // list of actual best match (ie the file is a best match for the
+ // device/state)
+ List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
+
+ // get a locale that match the host locale roughly (may not be exact match on the region.)
+ int localeHostMatch = getLocaleMatch();
+
+ // build a list of combinations of non standard qualifiers to add to each device's
+ // qualifier set when testing for a match.
+ // These qualifiers are: locale, night-mode, car dock.
+ List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
+
+ // If the edited file has locales, then we have to select a matching locale from
+ // the list.
+ // However, if it doesn't, we don't randomly take the first locale, we take one
+ // matching the current host locale (making sure it actually exist in the project)
+ int start, max;
+ if (editedConfig.getLocaleQualifier() != null || localeHostMatch == -1) {
+ // add all the locales
+ start = 0;
+ max = localeList.size();
+ } else {
+ // only add the locale host match
+ start = localeHostMatch;
+ max = localeHostMatch + 1; // test is <
+ }
+
+ for (int i = start ; i < max ; i++) {
+ Locale l = localeList.get(i);
+
+ ConfigBundle bundle = new ConfigBundle();
+ bundle.config.setLocaleQualifier(l.qualifier);
+
+ bundle.localeIndex = i;
+ configBundles.add(bundle);
+ }
+
+ // add the dock mode to the bundle combinations.
+ addDockModeToBundles(configBundles);
+
+ // add the night mode to the bundle combinations.
+ addNightModeToBundles(configBundles);
+
+ addRenderTargetToBundles(configBundles);
+
+ for (Device device : devices) {
+ for (State state : device.getAllStates()) {
+
+ // loop on the list of config bundles to create full
+ // configurations.
+ FolderConfiguration stateConfig = DeviceConfigHelper.getFolderConfig(state);
+ for (ConfigBundle bundle : configBundles) {
+ // create a new config with device config
+ FolderConfiguration testConfig = new FolderConfiguration();
+ testConfig.set(stateConfig);
+
+ // add on top of it, the extra qualifiers from the bundle
+ testConfig.add(bundle.config);
+
+ if (editedConfig.isMatchFor(testConfig)) {
+ // this is a basic match. record it in case we don't
+ // find a match
+ // where the edited file is a best config.
+ anyMatches.add(new ConfigMatch(testConfig, device, state, bundle));
+
+ if (isCurrentFileBestMatchFor(testConfig)) {
+ // this is what we want.
+ bestMatches.add(new ConfigMatch(testConfig, device, state, bundle));
+ }
+ }
+ }
+ }
+ }
+
+ if (bestMatches.size() == 0) {
+ if (favorCurrentConfig) {
+ // quick check
+ if (!editedConfig.isMatchFor(currentConfig)) {
+ AdtPlugin.log(IStatus.ERROR,
+ "favorCurrentConfig can only be true if the current config is compatible");
+ }
+
+ // just display the warning
+ AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
+ String.format(
+ "'%1$s' is not a best match for any device/locale combination.",
+ editedConfig.toDisplayString()),
+ String.format(
+ "Displaying it with '%1$s'",
+ currentConfig.toDisplayString()));
+ } else if (anyMatches.size() > 0) {
+ // select the best device anyway.
+ ConfigMatch match = selectConfigMatch(anyMatches);
+ mConfiguration.setDevice(match.device, true);
+ mConfiguration.setDeviceState(match.state, true);
+ mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true);
+ mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true);
+ mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex),
+ true);
+
+ if (mUpdateUi) {
+ mConfigChooser.selectDevice(mConfiguration.getDevice());
+ mConfigChooser.selectDeviceState(mConfiguration.getDeviceState());
+ mConfigChooser.selectLocale(mConfiguration.getLocale());
+ }
+
+ mConfiguration.syncFolderConfig();
+
+ // TODO: display a better warning!
+ AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
+ String.format(
+ "'%1$s' is not a best match for any device/locale combination.",
+ editedConfig.toDisplayString()),
+ String.format(
+ "Displaying it with '%1$s' which is compatible, but will " +
+ "actually be displayed with another more specific version of " +
+ "the layout.",
+ currentConfig.toDisplayString()));
+
+ } else {
+ // TODO: there is no device/config able to display the layout, create one.
+ // For the base config values, we'll take the first device and state,
+ // and replace whatever qualifier required by the layout file.
+ }
+ } else {
+ ConfigMatch match = selectConfigMatch(bestMatches);
+ mConfiguration.setDevice(match.device, true);
+ mConfiguration.setDeviceState(match.state, true);
+ mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true);
+ mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true);
+ mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true);
+
+ mConfiguration.syncFolderConfig();
+
+ if (mUpdateUi) {
+ mConfigChooser.selectDevice(mConfiguration.getDevice());
+ mConfigChooser.selectDeviceState(mConfiguration.getDeviceState());
+ mConfigChooser.selectLocale(mConfiguration.getLocale());
+ }
+ }
+ }
+
+ private void addRenderTargetToBundles(List<ConfigBundle> configBundles) {
+ Pair<Locale, IAndroidTarget> state = Configuration.loadRenderState(mConfigChooser);
+ if (state != null) {
+ IAndroidTarget target = state.getSecond();
+ if (target != null) {
+ int apiLevel = target.getVersion().getApiLevel();
+ for (ConfigBundle bundle : configBundles) {
+ bundle.config.setVersionQualifier(
+ new VersionQualifier(apiLevel));
+ }
+ }
+ }
+ }
+
+ private void addDockModeToBundles(List<ConfigBundle> addConfig) {
+ ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
+
+ // loop on each item and for each, add all variations of the dock modes
+ for (ConfigBundle bundle : addConfig) {
+ int index = 0;
+ for (UiMode mode : UiMode.values()) {
+ ConfigBundle b = new ConfigBundle(bundle);
+ b.config.setUiModeQualifier(new UiModeQualifier(mode));
+ b.dockModeIndex = index++;
+ list.add(b);
+ }
+ }
+
+ addConfig.clear();
+ addConfig.addAll(list);
+ }
+
+ private void addNightModeToBundles(List<ConfigBundle> addConfig) {
+ ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
+
+ // loop on each item and for each, add all variations of the night modes
+ for (ConfigBundle bundle : addConfig) {
+ int index = 0;
+ for (NightMode mode : NightMode.values()) {
+ ConfigBundle b = new ConfigBundle(bundle);
+ b.config.setNightModeQualifier(new NightModeQualifier(mode));
+ b.nightModeIndex = index++;
+ list.add(b);
+ }
+ }
+
+ addConfig.clear();
+ addConfig.addAll(list);
+ }
+
+ private int getLocaleMatch() {
+ java.util.Locale defaultLocale = java.util.Locale.getDefault();
+ if (defaultLocale != null) {
+ String currentLanguage = defaultLocale.getLanguage();
+ String currentRegion = defaultLocale.getCountry();
+
+ List<Locale> localeList = mConfigChooser.getLocaleList();
+ final int count = localeList.size();
+ for (int l = 0; l < count; l++) {
+ Locale locale = localeList.get(l);
+ LocaleQualifier qualifier = locale.qualifier;
+
+ // there's always a ##/Other or ##/Any (which is the same, the region
+ // contains FAKE_REGION_VALUE). If we don't find a perfect region match
+ // we take the fake region. Since it's last in the list, this makes the
+ // test easy.
+ if (qualifier.getLanguage().equals(currentLanguage) &&
+ (qualifier.getRegion() == null || qualifier.getRegion().equals(currentRegion))) {
+ return l;
+ }
+ }
+
+ // if no locale match the current local locale, it's likely that it is
+ // the default one which is the last one.
+ return count - 1;
+ }
+
+ return -1;
+ }
+
+ private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) {
+ // API 11-13: look for a x-large device
+ Comparator<ConfigMatch> comparator = null;
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ IAndroidTarget projectTarget = sdk.getTarget(mEditedFile.getProject());
+ if (projectTarget != null) {
+ int apiLevel = projectTarget.getVersion().getApiLevel();
+ if (apiLevel >= 11 && apiLevel < 14) {
+ // TODO: Maybe check the compatible-screen tag in the manifest to figure out
+ // what kind of device should be used for display.
+ comparator = new TabletConfigComparator();
+ }
+ }
+ }
+ if (comparator == null) {
+ // lets look for a high density device
+ comparator = new PhoneConfigComparator();
+ }
+ Collections.sort(matches, comparator);
+
+ // Look at the currently active editor to see if it's a layout editor, and if so,
+ // look up its configuration and if the configuration is in our match list,
+ // use it. This means we "preserve" the current configuration when you open
+ // new layouts.
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ if (delegate != null
+ // (Only do this when the two files are in the same project)
+ && delegate.getEditor().getProject() == mEditedFile.getProject()) {
+ FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration();
+ if (configuration != null) {
+ for (ConfigMatch match : matches) {
+ if (configuration.equals(match.testConfig)) {
+ return match;
+ }
+ }
+ }
+ }
+
+ // the list has been sorted so that the first item is the best config
+ return matches.get(0);
+ }
+
+ /** Return the default render target to use, or null if no strong preference */
+ @Nullable
+ static IAndroidTarget findDefaultRenderTarget(ConfigurationChooser chooser) {
+ if (PREFER_RECENT_RENDER_TARGETS) {
+ // Use the most recent target
+ List<IAndroidTarget> targetList = chooser.getTargetList();
+ if (!targetList.isEmpty()) {
+ return targetList.get(targetList.size() - 1);
+ }
+ }
+
+ IProject project = chooser.getProject();
+ // Default to layoutlib version 5
+ Sdk current = Sdk.getCurrent();
+ if (current != null) {
+ IAndroidTarget projectTarget = current.getTarget(project);
+ int minProjectApi = Integer.MAX_VALUE;
+ if (projectTarget != null) {
+ if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) {
+ // Renderable non-platform targets are all going to be adequate (they
+ // will have at least version 5 of layoutlib) so use the project
+ // target as the render target.
+ return projectTarget;
+ }
+
+ if (projectTarget.getVersion().isPreview()
+ && projectTarget.hasRenderingLibrary()) {
+ // If the project target is a preview version, then just use it
+ return projectTarget;
+ }
+
+ minProjectApi = projectTarget.getVersion().getApiLevel();
+ }
+
+ // We want to pick a render target that contains at least version 5 (and
+ // preferably version 6) of the layout library. To do this, we go through the
+ // targets and pick the -smallest- API level that is both simultaneously at
+ // least as big as the project API level, and supports layoutlib level 5+.
+ IAndroidTarget best = null;
+ int bestApiLevel = Integer.MAX_VALUE;
+
+ for (IAndroidTarget target : current.getTargets()) {
+ // Non-platform targets are not chosen as the default render target
+ if (!target.isPlatform()) {
+ continue;
+ }
+
+ int apiLevel = target.getVersion().getApiLevel();
+
+ // Ignore targets that have a lower API level than the minimum project
+ // API level:
+ if (apiLevel < minProjectApi) {
+ continue;
+ }
+
+ // Look up the layout lib API level. This property is new so it will only
+ // be defined for version 6 or higher, which means non-null is adequate
+ // to see if this target is eligible:
+ String property = target.getProperty(PkgProps.LAYOUTLIB_API);
+ // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate:
+ if (property != null || apiLevel >= 11) {
+ if (apiLevel < bestApiLevel) {
+ bestApiLevel = apiLevel;
+ best = target;
+ }
+ }
+ }
+
+ return best;
+ }
+
+ return null;
+ }
+
+ /**
+ * Attempts to find a close state among a list
+ *
+ * @param oldConfig the reference config.
+ * @param states the list of states to search through
+ * @return the name of the closest state match, or possibly null if no states are compatible
+ * (this can only happen if the states don't have a single qualifier that is the same).
+ */
+ @Nullable
+ static String getClosestMatch(@NonNull FolderConfiguration oldConfig,
+ @NonNull List<State> states) {
+
+ // create 2 lists as we're going to go through one and put the
+ // candidates in the other.
+ List<State> list1 = new ArrayList<State>(states.size());
+ List<State> list2 = new ArrayList<State>(states.size());
+
+ list1.addAll(states);
+
+ final int count = FolderConfiguration.getQualifierCount();
+ for (int i = 0 ; i < count ; i++) {
+ // compute the new candidate list by only taking states that have
+ // the same i-th qualifier as the old state
+ for (State s : list1) {
+ ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
+
+ FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s);
+ ResourceQualifier newQualifier =
+ folderConfig != null ? folderConfig.getQualifier(i) : null;
+
+ if (oldQualifier == null) {
+ if (newQualifier == null) {
+ list2.add(s);
+ }
+ } else if (oldQualifier.equals(newQualifier)) {
+ list2.add(s);
+ }
+ }
+
+ // at any moment if the new candidate list contains only one match, its name
+ // is returned.
+ if (list2.size() == 1) {
+ return list2.get(0).getName();
+ }
+
+ // if the list is empty, then all the new states failed. It is considered ok, and
+ // we move to the next qualifier anyway. This way, if a qualifier is different for
+ // all new states it is simply ignored.
+ if (list2.size() != 0) {
+ // move the candidates back into list1.
+ list1.clear();
+ list1.addAll(list2);
+ list2.clear();
+ }
+ }
+
+ // the only way to reach this point is if there's an exact match.
+ // (if there are more than one, then there's a duplicate state and it doesn't matter,
+ // we take the first one).
+ if (list1.size() > 0) {
+ return list1.get(0).getName();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the layout {@link IFile} which best matches the configuration
+ * selected in the given configuration chooser.
+ *
+ * @param chooser the associated configuration chooser holding project state
+ * @return the file which best matches the settings
+ */
+ @Nullable
+ public static IFile getBestFileMatch(ConfigurationChooser chooser) {
+ // get the resources of the file's project.
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(chooser.getProject());
+ if (resources == null) {
+ return null;
+ }
+
+ // From the resources, look for a matching file
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile == null) {
+ return null;
+ }
+ String name = editedFile.getName();
+ FolderConfiguration config = chooser.getConfiguration().getFullConfig();
+ ResourceFile match = resources.getMatchingFile(name, ResourceType.LAYOUT, config);
+
+ if (match != null) {
+ // In Eclipse, the match's file is always an instance of IFileWrapper
+ return ((IFileWrapper) match.getFile()).getIFile();
+ }
+
+ return null;
+ }
+
+ /**
+ * Note: this comparator imposes orderings that are inconsistent with equals.
+ */
+ private static class TabletConfigComparator implements Comparator<ConfigMatch> {
+ @Override
+ public int compare(ConfigMatch o1, ConfigMatch o2) {
+ FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
+ FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
+ if (config1 == null) {
+ if (config2 == null) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (config2 == null) {
+ return 1;
+ }
+
+ ScreenSizeQualifier size1 = config1.getScreenSizeQualifier();
+ ScreenSizeQualifier size2 = config2.getScreenSizeQualifier();
+ ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL;
+ ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL;
+
+ // X-LARGE is better than all others (which are considered identical)
+ // if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
+
+ if (ss1 == ScreenSize.XLARGE) {
+ if (ss2 == ScreenSize.XLARGE) {
+ ScreenOrientationQualifier orientation1 =
+ config1.getScreenOrientationQualifier();
+ ScreenOrientation so1 = orientation1.getValue();
+ if (so1 == null) {
+ so1 = ScreenOrientation.PORTRAIT;
+ }
+ ScreenOrientationQualifier orientation2 =
+ config2.getScreenOrientationQualifier();
+ ScreenOrientation so2 = orientation2.getValue();
+ if (so2 == null) {
+ so2 = ScreenOrientation.PORTRAIT;
+ }
+
+ if (so1 == ScreenOrientation.LANDSCAPE) {
+ if (so2 == ScreenOrientation.LANDSCAPE) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (so2 == ScreenOrientation.LANDSCAPE) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else {
+ return -1;
+ }
+ } else if (ss2 == ScreenSize.XLARGE) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ /**
+ * Note: this comparator imposes orderings that are inconsistent with equals.
+ */
+ private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
+
+ private final SparseIntArray mDensitySort = new SparseIntArray(4);
+
+ public PhoneConfigComparator() {
+ // put the sort order for the density.
+ mDensitySort.put(Density.HIGH.getDpiValue(), 1);
+ mDensitySort.put(Density.MEDIUM.getDpiValue(), 2);
+ mDensitySort.put(Density.XHIGH.getDpiValue(), 3);
+ mDensitySort.put(Density.LOW.getDpiValue(), 4);
+ }
+
+ @Override
+ public int compare(ConfigMatch o1, ConfigMatch o2) {
+ FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
+ FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
+ if (config1 == null) {
+ if (config2 == null) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (config2 == null) {
+ return 1;
+ }
+
+ int dpi1 = Density.DEFAULT_DENSITY;
+ int dpi2 = Density.DEFAULT_DENSITY;
+
+ DensityQualifier dpiQualifier1 = config1.getDensityQualifier();
+ if (dpiQualifier1 != null) {
+ Density value = dpiQualifier1.getValue();
+ dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
+ }
+ dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/);
+
+ DensityQualifier dpiQualifier2 = config2.getDensityQualifier();
+ if (dpiQualifier2 != null) {
+ Density value = dpiQualifier2.getValue();
+ dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
+ }
+ dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/);
+
+ if (dpi1 == dpi2) {
+ // portrait is better
+ ScreenOrientation so1 = ScreenOrientation.PORTRAIT;
+ ScreenOrientationQualifier orientationQualifier1 =
+ config1.getScreenOrientationQualifier();
+ if (orientationQualifier1 != null) {
+ so1 = orientationQualifier1.getValue();
+ if (so1 == null) {
+ so1 = ScreenOrientation.PORTRAIT;
+ }
+ }
+ ScreenOrientation so2 = ScreenOrientation.PORTRAIT;
+ ScreenOrientationQualifier orientationQualifier2 =
+ config2.getScreenOrientationQualifier();
+ if (orientationQualifier2 != null) {
+ so2 = orientationQualifier2.getValue();
+ if (so2 == null) {
+ so2 = ScreenOrientation.PORTRAIT;
+ }
+ }
+
+ if (so1 == ScreenOrientation.PORTRAIT) {
+ if (so2 == ScreenOrientation.PORTRAIT) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (so2 == ScreenOrientation.PORTRAIT) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ return dpi1 - dpi2;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java
new file mode 100644
index 000000000..a791c63f8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.LOCALES;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.VARIATIONS;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+
+import java.util.List;
+
+/**
+ * The {@linkplain ConfigurationMenuListener} class is responsible for
+ * generating the configuration menu in the {@link ConfigurationChooser}.
+ */
+class ConfigurationMenuListener extends SelectionAdapter {
+ private static final String ICON_NEW_CONFIG = "newConfig"; //$NON-NLS-1$
+ private static final int ACTION_SELECT_CONFIG = 1;
+ private static final int ACTION_CREATE_CONFIG_FILE = 2;
+ private static final int ACTION_ADD = 3;
+ private static final int ACTION_DELETE_ALL = 4;
+ private static final int ACTION_PREVIEW_MODE = 5;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final int mAction;
+ private final IFile mResource;
+ private final RenderPreviewMode mMode;
+
+ ConfigurationMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ int action,
+ @Nullable IFile resource,
+ @Nullable RenderPreviewMode mode) {
+ mConfigChooser = configChooser;
+ mAction = action;
+ mResource = resource;
+ mMode = mode;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switch (mAction) {
+ case ACTION_SELECT_CONFIG: {
+ try {
+ AdtPlugin.openFile(mResource, null, false);
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, null);
+ }
+ return;
+ }
+ case ACTION_CREATE_CONFIG_FILE: {
+ ConfigurationClient client = mConfigChooser.getClient();
+ if (client != null) {
+ client.createConfigFile();
+ }
+ return;
+ }
+ }
+
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ IFile editedFile = mConfigChooser.getEditedFile();
+
+ if (delegate == null || editedFile == null) {
+ return;
+ }
+ // (Only do this when the two files are in the same project)
+ IProject project = delegate.getEditor().getProject();
+ if (project == null ||
+ !project.equals(editedFile.getProject())) {
+ return;
+ }
+ LayoutCanvas canvas = delegate.getGraphicalEditor().getCanvasControl();
+ RenderPreviewManager previewManager = canvas.getPreviewManager();
+
+ switch (mAction) {
+ case ACTION_ADD: {
+ previewManager.addAsThumbnail();
+ break;
+ }
+ case ACTION_PREVIEW_MODE: {
+ previewManager.selectMode(mMode);
+ break;
+ }
+ case ACTION_DELETE_ALL: {
+ previewManager.deleteManualPreviews();
+ break;
+ }
+ default: assert false : mAction;
+ }
+ canvas.setFitScale(true /*onlyZoomOut*/, false /*allowZoomIn*/);
+ canvas.redraw();
+ }
+
+ static void show(ConfigurationChooser chooser, ToolItem combo) {
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ RenderPreviewMode mode = AdtPrefs.getPrefs().getRenderPreviewMode();
+
+ // Configuration Previews
+ create(menu, "Add As Thumbnail...",
+ new ConfigurationMenuListener(chooser, ACTION_ADD, null, null),
+ SWT.PUSH, false);
+ if (mode == RenderPreviewMode.CUSTOM) {
+ MenuItem item = create(menu, "Delete All Thumbnails",
+ new ConfigurationMenuListener(chooser, ACTION_DELETE_ALL, null, null),
+ SWT.PUSH, false);
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ if (delegate != null) {
+ LayoutCanvas canvas = delegate.getGraphicalEditor().getCanvasControl();
+ RenderPreviewManager previewManager = canvas.getPreviewManager();
+ if (!previewManager.hasManualPreviews()) {
+ item.setEnabled(false);
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem configSeparator = new MenuItem(menu, SWT.SEPARATOR);
+
+ create(menu, "Preview Representative Sample",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ DEFAULT), SWT.RADIO, mode == DEFAULT);
+ create(menu, "Preview All Screen Sizes",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ SCREENS), SWT.RADIO, mode == SCREENS);
+
+ MenuItem localeItem = create(menu, "Preview All Locales",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ LOCALES), SWT.RADIO, mode == LOCALES);
+ if (chooser.getLocaleList().size() <= 1) {
+ localeItem.setEnabled(false);
+ }
+
+ boolean canPreviewIncluded = false;
+ IProject project = chooser.getProject();
+ if (project != null) {
+ IncludeFinder finder = IncludeFinder.get(project);
+ final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile());
+ canPreviewIncluded = includedBy != null && !includedBy.isEmpty();
+ }
+ //if (!graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ // canPreviewIncluded = false;
+ //}
+ MenuItem includedItem = create(menu, "Preview Included",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ INCLUDES), SWT.RADIO, mode == INCLUDES);
+ if (!canPreviewIncluded) {
+ includedItem.setEnabled(false);
+ }
+
+ IFile file = chooser.getEditedFile();
+ List<IFile> variations = AdtUtils.getResourceVariations(file, true);
+ MenuItem variationsItem = create(menu, "Preview Layout Versions",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ VARIATIONS), SWT.RADIO, mode == VARIATIONS);
+ if (variations.size() <= 1) {
+ variationsItem.setEnabled(false);
+ }
+
+ create(menu, "Manual Previews",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ CUSTOM), SWT.RADIO, mode == CUSTOM);
+ create(menu, "None",
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null,
+ NONE), SWT.RADIO, mode == NONE);
+
+ if (variations.size() > 1) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ ResourceManager manager = ResourceManager.getInstance();
+ for (final IFile resource : variations) {
+ IFolder parent = (IFolder) resource.getParent();
+ ResourceFolder parentResource = manager.getResourceFolder(parent);
+ FolderConfiguration configuration = parentResource.getConfiguration();
+ String title = configuration.toDisplayString();
+
+ MenuItem item = create(menu, title,
+ new ConfigurationMenuListener(chooser, ACTION_SELECT_CONFIG,
+ resource, null),
+ SWT.CHECK, false);
+
+ if (file != null) {
+ boolean selected = file.equals(resource);
+ if (selected) {
+ item.setSelection(true);
+ item.setEnabled(false);
+ }
+ }
+ }
+ }
+
+ Configuration configuration = chooser.getConfiguration();
+ if (configuration.getEditedConfig() != null &&
+ !configuration.getEditedConfig().equals(configuration.getFullConfig())) {
+ if (variations.size() > 0) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ // Add action for creating a new configuration
+ MenuItem item = create(menu, "Create New...",
+ new ConfigurationMenuListener(chooser, ACTION_CREATE_CONFIG_FILE,
+ null, null),
+ SWT.PUSH, false);
+ item.setImage(IconFactory.getInstance().getIcon(ICON_NEW_CONFIG));
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ @NonNull
+ public static MenuItem create(@NonNull Menu menu, String title,
+ ConfigurationMenuListener listener, int style, boolean selected) {
+ MenuItem item = new MenuItem(menu, style);
+ item.setText(title);
+ item.addSelectionListener(listener);
+ if (selected) {
+ item.setSelection(true);
+ }
+ return item;
+ }
+
+ @NonNull
+ static MenuItem addTogglePreviewModeAction(
+ @NonNull Menu menu,
+ @NonNull String title,
+ @NonNull ConfigurationChooser chooser,
+ @NonNull RenderPreviewMode mode) {
+ boolean selected = AdtPrefs.getPrefs().getRenderPreviewMode() == mode;
+ if (selected) {
+ mode = RenderPreviewMode.NONE;
+ }
+ return create(menu, title,
+ new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, mode),
+ SWT.CHECK, selected);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java
new file mode 100644
index 000000000..72910f9cc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.ide.common.rendering.HardwareConfigHelper.MANUFACTURER_GENERIC;
+import static com.android.ide.common.rendering.HardwareConfigHelper.getGenericLabel;
+import static com.android.ide.common.rendering.HardwareConfigHelper.getNexusLabel;
+import static com.android.ide.common.rendering.HardwareConfigHelper.isGeneric;
+import static com.android.ide.common.rendering.HardwareConfigHelper.isNexus;
+import static com.android.ide.common.rendering.HardwareConfigHelper.sortNexusList;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * The {@linkplain DeviceMenuListener} class is responsible for generating the device
+ * menu in the {@link ConfigurationChooser}.
+ */
+class DeviceMenuListener extends SelectionAdapter {
+ private final ConfigurationChooser mConfigChooser;
+ private final Device mDevice;
+
+ DeviceMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ @Nullable Device device) {
+ mConfigChooser = configChooser;
+ mDevice = device;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mConfigChooser.selectDevice(mDevice);
+ mConfigChooser.onDeviceChange();
+ }
+
+ static void show(final ConfigurationChooser chooser, ToolItem combo) {
+ Configuration configuration = chooser.getConfiguration();
+ Device current = configuration.getDevice();
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+
+ Collection<Device> deviceCollection = chooser.getDevices();
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AvdManager avdManager = sdk.getAvdManager();
+ if (avdManager != null) {
+ boolean separatorNeeded = false;
+ AvdInfo[] avds = avdManager.getValidAvds();
+ for (AvdInfo avd : avds) {
+ for (Device device : deviceCollection) {
+ if (device.getManufacturer().equals(avd.getDeviceManufacturer())
+ && device.getName().equals(avd.getDeviceName())) {
+ separatorNeeded = true;
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(avd.getName());
+ item.setSelection(current == device);
+
+ item.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+ }
+ }
+
+ if (separatorNeeded) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+ }
+ }
+
+ // Group the devices by manufacturer, then put them in the menu.
+ // If we don't have anything but Nexus devices, group them together rather than
+ // make many manufacturer submenus.
+ boolean haveNexus = false;
+ boolean haveNonNexus = false;
+ if (!deviceCollection.isEmpty()) {
+ Map<String, List<Device>> manufacturers = new TreeMap<String, List<Device>>();
+ for (Device device : deviceCollection) {
+ List<Device> devices;
+ if (isNexus(device)) {
+ haveNexus = true;
+ } else if (!isGeneric(device)) {
+ haveNonNexus = true;
+ }
+ if (manufacturers.containsKey(device.getManufacturer())) {
+ devices = manufacturers.get(device.getManufacturer());
+ } else {
+ devices = new ArrayList<Device>();
+ manufacturers.put(device.getManufacturer(), devices);
+ }
+ devices.add(device);
+ }
+ if (haveNonNexus) {
+ for (List<Device> devices : manufacturers.values()) {
+ Menu manufacturerMenu = menu;
+ if (manufacturers.size() > 1) {
+ MenuItem item = new MenuItem(menu, SWT.CASCADE);
+ item.setText(devices.get(0).getManufacturer());
+ manufacturerMenu = new Menu(menu);
+ item.setMenu(manufacturerMenu);
+ }
+ for (final Device device : devices) {
+ MenuItem deviceItem = new MenuItem(manufacturerMenu, SWT.CHECK);
+ deviceItem.setText(getGenericLabel(device));
+ deviceItem.setSelection(current == device);
+ deviceItem.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+ }
+ } else {
+ List<Device> nexus = new ArrayList<Device>();
+ List<Device> generic = new ArrayList<Device>();
+ if (haveNexus) {
+ // Nexus
+ for (List<Device> devices : manufacturers.values()) {
+ for (Device device : devices) {
+ if (isNexus(device)) {
+ if (device.getManufacturer().equals(MANUFACTURER_GENERIC)) {
+ generic.add(device);
+ } else {
+ nexus.add(device);
+ }
+ } else {
+ generic.add(device);
+ }
+ }
+ }
+ }
+
+ if (!nexus.isEmpty()) {
+ sortNexusList(nexus);
+ for (final Device device : nexus) {
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(getNexusLabel(device));
+ item.setSelection(current == device);
+ item.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+ }
+
+ // Generate the generic menu.
+ Collections.reverse(generic);
+ for (final Device device : generic) {
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(getGenericLabel(device));
+ item.setSelection(current == device);
+ item.addSelectionListener(new DeviceMenuListener(chooser, device));
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ ConfigurationMenuListener.addTogglePreviewModeAction(menu,
+ "Preview All Screens", chooser, RenderPreviewMode.SCREENS);
+
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java
new file mode 100644
index 000000000..15623cf30
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/FlagManager.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.LocaleManager;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.google.common.collect.Maps;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.wb.internal.core.DesignerPlugin;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The {@linkplain FlagManager} provides access to flags for regions known
+ * to {@link LocaleManager}. It also contains some locale related display
+ * functions.
+ * <p>
+ * All the flag images came from the WindowBuilder subversion repository
+ * http://dev.eclipse.org/svnroot/tools/org.eclipse.windowbuilder/trunk (and in
+ * particular, a snapshot of revision 424). However, it appears that the icons
+ * are from http://www.famfamfam.com/lab/icons/flags/ which states that "these
+ * flag icons are available for free use for any purpose with no requirement for
+ * attribution." Adding the URL here such that we can check back occasionally
+ * and see if there are corrections or updates. Also note that the flag names
+ * are in ISO 3166-1 alpha-2 country codes.
+ */
+public class FlagManager {
+ private static final FlagManager sInstance = new FlagManager();
+
+ /**
+ * Returns the {@linkplain FlagManager} singleton
+ *
+ * @return the {@linkplain FlagManager} singleton, never null
+ */
+ @NonNull
+ public static FlagManager get() {
+ return sInstance;
+ }
+
+ /** Use the {@link #get()} factory method */
+ private FlagManager() {
+ }
+
+ /** Map from region to flag icon */
+ private final Map<String, Image> mImageMap = Maps.newHashMap();
+
+ /**
+ * Returns the empty flag icon used to indicate an unknown country
+ *
+ * @return the globe icon used to indicate an unknown country
+ */
+ public static Image getEmptyIcon() {
+ return DesignerPlugin.getImage("nls/flags/flag_empty.png"); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the globe icon used to indicate "any" language
+ *
+ * @return the globe icon used to indicate "any" language
+ */
+ public static Image getGlobeIcon() {
+ return IconFactory.getInstance().getIcon("globe"); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the flag for the given language and region.
+ *
+ * @param language the language, or null (if null, region must not be null),
+ * the 2 letter language code (ISO 639-1), in lower case
+ * @param region the region, or null (if null, language must not be null),
+ * the 2 letter region code (ISO 3166-1 alpha-2), in upper case
+ * @return a suitable flag icon, or null
+ */
+ @Nullable
+ public Image getFlag(@Nullable String language, @Nullable String region) {
+ assert region != null || language != null;
+ if (region == null || region.isEmpty()) {
+ // Look up the region for a given language
+ assert language != null;
+
+ // Special cases where we have a dedicated flag available:
+ if (language.equals("ca")) { //$NON-NLS-1$
+ return getIcon("catalonia"); //$NON-NLS-1$
+ }
+ else if (language.equals("gd")) { //$NON-NLS-1$
+ return getIcon("scotland"); //$NON-NLS-1$
+ }
+ else if (language.equals("cy")) { //$NON-NLS-1$
+ return getIcon("wales"); //$NON-NLS-1$
+ }
+
+ // Prefer the local registration of the current locale; even if
+ // for example the default locale for English is the US, if the current
+ // default locale is English, then use its associated country, which could
+ // for example be Australia.
+ Locale locale = Locale.getDefault();
+ if (language.equals(locale.getLanguage())) {
+ Image flag = getFlag(locale.getCountry());
+ if (flag != null) {
+ return flag;
+ }
+ }
+
+ region = LocaleManager.getLanguageRegion(language);
+ }
+
+ if (region == null || region.isEmpty()) {
+ // No country specified, and the language is for a country we
+ // don't have a flag for
+ return null;
+ }
+
+ return getIcon(region);
+ }
+
+ /**
+ * Returns the flag for the given language and region.
+ *
+ * @param language the language qualifier, or null (if null, region must not be null),
+ * @param region the region, or null (if null, language must not be null),
+ * @return a suitable flag icon, or null
+ */
+ public Image getFlag(@Nullable LocaleQualifier locale) {
+ if (locale == null) {
+ return null;
+ }
+ String languageCode = locale.getLanguage();
+ String regionCode = locale.getRegion();
+ if (LocaleQualifier.FAKE_VALUE.equals(languageCode)) {
+ languageCode = null;
+ }
+ return getFlag(languageCode, regionCode);
+ }
+
+ /**
+ * Returns a flag for a given resource folder name (such as
+ * {@code values-en-rUS}), or null
+ *
+ * @param folder the folder name
+ * @return a corresponding flag icon, or null if none was found
+ */
+ @Nullable
+ public Image getFlagForFolderName(@NonNull String folder) {
+ FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(folder);
+ if (configuration != null) {
+ return get().getFlag(configuration);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the flag for the given folder
+ *
+ * @param configuration the folder configuration
+ * @return a suitable flag icon, or null
+ */
+ @Nullable
+ public Image getFlag(@NonNull FolderConfiguration configuration) {
+ return getFlag(configuration.getLocaleQualifier());
+ }
+
+
+
+ /**
+ * Returns the flag for the given region.
+ *
+ * @param region the 2 letter region code (ISO 3166-1 alpha-2), in upper case
+ * @return a suitable flag icon, or null
+ */
+ @Nullable
+ public Image getFlag(@NonNull String region) {
+ assert region.length() == 2
+ && Character.isUpperCase(region.charAt(0))
+ && Character.isUpperCase(region.charAt(1)) : region;
+
+ return getIcon(region);
+ }
+
+ private Image getIcon(@NonNull String base) {
+ Image flagImage = mImageMap.get(base);
+ if (flagImage == null) {
+ // TODO: Special case locale currently running on system such
+ // that the current country matches the current locale
+ if (mImageMap.containsKey(base)) {
+ // Already checked: there's just no image there
+ return null;
+ }
+ String flagFileName = base.toLowerCase(Locale.US) + ".png"; //$NON-NLS-1$
+ flagImage = DesignerPlugin.getImage("nls/flags/" + flagFileName); //$NON-NLS-1$
+ mImageMap.put(base, flagImage);
+ }
+
+ return flagImage;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java
new file mode 100644
index 000000000..97ff66845
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode;
+import com.android.resources.ResourceFolderType;
+import com.android.sdkuilib.ui.GridDialog;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Dialog to choose a non existing {@link FolderConfiguration}.
+ */
+public final class LayoutCreatorDialog extends GridDialog {
+
+ private ConfigurationSelector mSelector;
+ private Composite mStatusComposite;
+ private Label mStatusLabel;
+ private Label mStatusImage;
+
+ private final FolderConfiguration mConfig = new FolderConfiguration();
+ private final String mFileName;
+
+ /**
+ * Creates a dialog, and init the UI from a {@link FolderConfiguration}.
+ * @param parentShell the parent {@link Shell}.
+ * @param fileName the filename associated with the configuration
+ * @param config The starting configuration.
+ */
+ public LayoutCreatorDialog(Shell parentShell, String fileName, FolderConfiguration config) {
+ super(parentShell, 1, false);
+
+ mFileName = fileName;
+
+ // FIXME: add some data to know what configurations already exist.
+ mConfig.set(config);
+ }
+
+ @Override
+ public void createDialogContent(Composite parent) {
+ new Label(parent, SWT.NONE).setText(
+ String.format("Configuration for the alternate version of %1$s", mFileName));
+
+ mSelector = new ConfigurationSelector(parent, SelectorMode.CONFIG_ONLY);
+ mSelector.setConfiguration(mConfig);
+
+ // because the ConfigSelector is running in CONFIG_ONLY mode, the current config
+ // displayed by it is not mConfig anymore, so get the current config.
+ mSelector.getConfiguration(mConfig);
+
+ // parent's layout is a GridLayout as specified in the javadoc.
+ GridData gd = new GridData();
+ gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mSelector.setLayoutData(gd);
+
+ // add a listener to check on the validity of the FolderConfiguration as
+ // they are built.
+ mSelector.setOnChangeListener(new Runnable() {
+ @Override
+ public void run() {
+ ConfigurationState state = mSelector.getState();
+
+ switch (state) {
+ case OK:
+ mSelector.getConfiguration(mConfig);
+
+ resetStatus();
+ mStatusImage.setImage(null);
+ getButton(IDialogConstants.OK_ID).setEnabled(true);
+ break;
+ case INVALID_CONFIG:
+ ResourceQualifier invalidQualifier = mSelector.getInvalidQualifier();
+ mStatusLabel.setText(String.format(
+ "Invalid Configuration: %1$s has no filter set.",
+ invalidQualifier.getName()));
+ mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$
+ getButton(IDialogConstants.OK_ID).setEnabled(false);
+ break;
+ case REGION_WITHOUT_LANGUAGE:
+ mStatusLabel.setText(
+ "The Region qualifier requires the Language qualifier.");
+ mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$
+ getButton(IDialogConstants.OK_ID).setEnabled(false);
+ break;
+ }
+
+ // need to relayout, because of the change in size in mErrorImage.
+ mStatusComposite.layout();
+ }
+ });
+
+ mStatusComposite = new Composite(parent, SWT.NONE);
+ mStatusComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ GridLayout gl = new GridLayout(2, false);
+ mStatusComposite.setLayout(gl);
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mStatusImage = new Label(mStatusComposite, SWT.NONE);
+ mStatusLabel = new Label(mStatusComposite, SWT.NONE);
+ mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ resetStatus();
+ }
+
+ /**
+ * Sets the edited configuration on the given configuration parameter
+ *
+ * @param config the configuration to apply the current edits to
+ */
+ public void getConfiguration(FolderConfiguration config) {
+ config.set(mConfig);
+ }
+
+ /**
+ * resets the status label to show the file that will be created.
+ */
+ private void resetStatus() {
+ String displayString = Dialog.shortenText(String.format("New File: res/%1$s/%2$s",
+ mConfig.getFolderName(ResourceFolderType.LAYOUT), mFileName),
+ mStatusLabel);
+ mStatusLabel.setText(displayString);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java
new file mode 100644
index 000000000..6cb396394
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Locale.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.ide.common.resources.configuration.LocaleQualifier.FAKE_VALUE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * A language,region pair
+ */
+public class Locale {
+ /**
+ * A special marker region qualifier representing any region
+ */
+ public static final LocaleQualifier ANY_QUALIFIER = new LocaleQualifier(FAKE_VALUE);
+
+ /**
+ * A locale which matches any language and region
+ */
+ public static final Locale ANY = new Locale(ANY_QUALIFIER);
+
+ /**
+ * The locale qualifier, or {@link #ANY_QUALIFIER} if this locale matches
+ * any locale
+ */
+ @NonNull
+ public final LocaleQualifier qualifier;
+
+ /**
+ * Constructs a new {@linkplain Locale} matching a given language in a given
+ * locale.
+ *
+ * @param locale the locale
+ */
+ private Locale(@NonNull
+ LocaleQualifier locale) {
+ qualifier = locale;
+ }
+
+ /**
+ * Constructs a new {@linkplain Locale} matching a given language in a given
+ * specific locale.
+ *
+ * @param locale the locale
+ * @return a locale with the given locale
+ */
+ @NonNull
+ public static Locale create(@NonNull
+ LocaleQualifier locale) {
+ return new Locale(locale);
+ }
+
+ /**
+ * Constructs a new {@linkplain Locale} for the given folder configuration
+ *
+ * @param folder the folder configuration
+ * @return a locale with the given language and region
+ */
+ public static Locale create(FolderConfiguration folder) {
+ LocaleQualifier locale = folder.getLocaleQualifier();
+ if (locale == null) {
+ return ANY;
+ } else {
+ return new Locale(locale);
+ }
+ }
+
+ /**
+ * Constructs a new {@linkplain Locale} for the given locale string, e.g.
+ * "zh", "en-rUS", or "b+eng+US".
+ *
+ * @param localeString the locale description
+ * @return the corresponding locale
+ */
+ @NonNull
+ public static Locale create(@NonNull
+ String localeString) {
+ // Load locale. Note that this can get overwritten by the
+ // project-wide settings read below.
+
+ LocaleQualifier qualifier = LocaleQualifier.getQualifier(localeString);
+ if (qualifier != null) {
+ return new Locale(qualifier);
+ } else {
+ return ANY;
+ }
+ }
+
+ /**
+ * Returns a flag image to use for this locale
+ *
+ * @return a flag image, or a default globe icon
+ */
+ @NonNull
+ public Image getFlagImage() {
+ String languageCode = qualifier.hasLanguage() ? qualifier.getLanguage() : null;
+ if (languageCode == null) {
+ return FlagManager.getGlobeIcon();
+ }
+ String regionCode = hasRegion() ? qualifier.getRegion() : null;
+ FlagManager icons = FlagManager.get();
+ Image image = icons.getFlag(languageCode, regionCode);
+ if (image != null) {
+ return image;
+ } else {
+ return FlagManager.getGlobeIcon();
+ }
+ }
+
+ /**
+ * Returns true if this locale specifies a specific language. This is true
+ * for all locales except {@link #ANY}.
+ *
+ * @return true if this locale specifies a specific language
+ */
+ public boolean hasLanguage() {
+ return !qualifier.hasFakeValue();
+ }
+
+ /**
+ * Returns true if this locale specifies a specific region
+ *
+ * @return true if this locale specifies a region
+ */
+ public boolean hasRegion() {
+ return qualifier.getRegion() != null && !FAKE_VALUE.equals(qualifier.getRegion());
+ }
+
+ /**
+ * Returns the locale formatted as language-region. If region is not set,
+ * language is returned. If language is not set, empty string is returned.
+ */
+ public String toLocaleId() {
+ return qualifier == ANY_QUALIFIER ? "" : qualifier.getTag();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + qualifier.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable
+ Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Locale other = (Locale) obj;
+ if (!qualifier.equals(other.qualifier))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return qualifier.getTag();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java
new file mode 100644
index 000000000..2bc5417b0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.AddTranslationDialog;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.List;
+
+/**
+ * The {@linkplain LocaleMenuListener} class is responsible for generating the locale
+ * menu in the {@link ConfigurationChooser}.
+ */
+class LocaleMenuListener extends SelectionAdapter {
+ private static final int ACTION_SET_LOCALE = 1;
+ private static final int ACTION_ADD_TRANSLATION = 2;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final int mAction;
+ private final Locale mLocale;
+
+ LocaleMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ int action,
+ @Nullable Locale locale) {
+ mConfigChooser = configChooser;
+ mAction = action;
+ mLocale = locale;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switch (mAction) {
+ case ACTION_SET_LOCALE: {
+ mConfigChooser.selectLocale(mLocale);
+ mConfigChooser.onLocaleChange();
+ break;
+ }
+ case ACTION_ADD_TRANSLATION: {
+ IProject project = mConfigChooser.getProject();
+ Shell shell = mConfigChooser.getShell();
+ AddTranslationDialog dialog = new AddTranslationDialog(shell, project);
+ dialog.open();
+ break;
+ }
+ default: assert false : mAction;
+ }
+ }
+
+ static void show(final ConfigurationChooser chooser, ToolItem combo) {
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ Configuration configuration = chooser.getConfiguration();
+ List<Locale> locales = chooser.getLocaleList();
+ Locale current = configuration.getLocale();
+
+ for (Locale locale : locales) {
+ String title = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+ Image image = locale.getFlagImage();
+ item.setImage(image);
+
+ boolean selected = current == locale;
+ if (selected) {
+ item.setSelection(true);
+ }
+
+ LocaleMenuListener listener = new LocaleMenuListener(chooser, ACTION_SET_LOCALE,
+ locale);
+ item.addSelectionListener(listener);
+ }
+
+ if (locales.size() > 1) {
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ ConfigurationMenuListener.addTogglePreviewModeAction(menu,
+ "Preview All Locales", chooser, RenderPreviewMode.LOCALES);
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ MenuItem item = new MenuItem(menu, SWT.PUSH);
+ item.setText("Add New Translation...");
+ LocaleMenuListener listener = new LocaleMenuListener(chooser,
+ ACTION_ADD_TRANSLATION, null);
+ item.addSelectionListener(listener);
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java
new file mode 100644
index 000000000..50778e2f1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.NightMode;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.google.common.base.Objects;
+
+/**
+ * An {@linkplain NestedConfiguration} is a {@link Configuration} which inherits
+ * all of its values from a different configuration, except for one or more
+ * attributes where it overrides a custom value.
+ * <p>
+ * Unlike a {@link VaryingConfiguration}, a {@linkplain NestedConfiguration}
+ * will always return the same overridden value, regardless of the inherited
+ * value.
+ * <p>
+ * For example, an {@linkplain NestedConfiguration} may fix the locale to always
+ * be "en", but otherwise inherit everything else.
+ */
+public class NestedConfiguration extends Configuration {
+ /** The configuration we are inheriting non-overridden values from */
+ protected Configuration mParent;
+
+ /** Bitmask of attributes to be overridden in this configuration */
+ private int mOverride;
+
+ /**
+ * Constructs a new {@linkplain NestedConfiguration}.
+ * Construct via
+ *
+ * @param chooser the associated chooser
+ * @param configuration the configuration to inherit from
+ */
+ protected NestedConfiguration(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull Configuration configuration) {
+ super(chooser);
+ mParent = configuration;
+
+ mFullConfig.set(mParent.mFullConfig);
+ if (mParent.getEditedConfig() != null) {
+ mEditedConfig = new FolderConfiguration();
+ mEditedConfig.set(mParent.mEditedConfig);
+ }
+ }
+
+ /**
+ * Returns the override flags for this configuration. Corresponds to
+ * the {@code CFG_} flags in {@link ConfigurationClient}.
+ *
+ * @return the bitmask
+ */
+ public int getOverrideFlags() {
+ return mOverride;
+ }
+
+ /**
+ * Creates a new {@linkplain NestedConfiguration} that has the same overriding
+ * attributes as the given other {@linkplain NestedConfiguration}, and gets
+ * its values from the given {@linkplain Configuration}.
+ *
+ * @param other the configuration to copy overrides from
+ * @param values the configuration to copy values from
+ * @param parent the parent to tie the configuration to for inheriting values
+ * @return a new configuration
+ */
+ @NonNull
+ public static NestedConfiguration create(
+ @NonNull NestedConfiguration other,
+ @NonNull Configuration values,
+ @NonNull Configuration parent) {
+ NestedConfiguration configuration =
+ new NestedConfiguration(other.mConfigChooser, parent);
+ initFrom(configuration, other, values, true /*sync*/);
+ return configuration;
+ }
+
+ /**
+ * Initializes a new {@linkplain NestedConfiguration} with the overriding
+ * attributes as the given other {@linkplain NestedConfiguration}, and gets
+ * its values from the given {@linkplain Configuration}.
+ *
+ * @param configuration the configuration to initialize
+ * @param other the configuration to copy overrides from
+ * @param values the configuration to copy values from
+ * @param sync if true, sync the folder configuration from
+ */
+ protected static void initFrom(NestedConfiguration configuration,
+ NestedConfiguration other, Configuration values, boolean sync) {
+ configuration.mOverride = other.mOverride;
+ configuration.setDisplayName(values.getDisplayName());
+ configuration.setActivity(values.getActivity());
+
+ if (configuration.isOverridingLocale()) {
+ configuration.setLocale(values.getLocale(), true);
+ }
+ if (configuration.isOverridingTarget()) {
+ configuration.setTarget(values.getTarget(), true);
+ }
+ if (configuration.isOverridingDevice()) {
+ configuration.setDevice(values.getDevice(), true);
+ }
+ if (configuration.isOverridingDeviceState()) {
+ configuration.setDeviceState(values.getDeviceState(), true);
+ }
+ if (configuration.isOverridingNightMode()) {
+ configuration.setNightMode(values.getNightMode(), true);
+ }
+ if (configuration.isOverridingUiMode()) {
+ configuration.setUiMode(values.getUiMode(), true);
+ }
+ if (sync) {
+ configuration.syncFolderConfig();
+ }
+ }
+
+ /**
+ * Sets the parent configuration that this configuration is inheriting from.
+ *
+ * @param parent the parent configuration
+ */
+ public void setParent(@NonNull Configuration parent) {
+ mParent = parent;
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration} which inherits values from the
+ * given parent {@linkplain Configuration}, possibly overriding some as
+ * well.
+ *
+ * @param chooser the associated chooser
+ * @param parent the configuration to inherit values from
+ * @return a new configuration
+ */
+ @NonNull
+ public static NestedConfiguration create(@NonNull ConfigurationChooser chooser,
+ @NonNull Configuration parent) {
+ return new NestedConfiguration(chooser, parent);
+ }
+
+ @Override
+ @Nullable
+ public String getTheme() {
+ // Never overridden: this is a static attribute of a layout, not something which
+ // varies by configuration or at runtime
+ return mParent.getTheme();
+ }
+
+ @Override
+ public void setTheme(String theme) {
+ // Never overridden
+ mParent.setTheme(theme);
+ }
+
+ /**
+ * Sets whether the locale should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideLocale(boolean override) {
+ mOverride |= CFG_LOCALE;
+ }
+
+ /**
+ * Returns true if the locale is overridden
+ *
+ * @return true if the locale is overridden
+ */
+ public final boolean isOverridingLocale() {
+ return (mOverride & CFG_LOCALE) != 0;
+ }
+
+ @Override
+ @NonNull
+ public Locale getLocale() {
+ if (isOverridingLocale()) {
+ return super.getLocale();
+ } else {
+ return mParent.getLocale();
+ }
+ }
+
+ @Override
+ public void setLocale(@NonNull Locale locale, boolean skipSync) {
+ if (isOverridingLocale()) {
+ super.setLocale(locale, skipSync);
+ } else {
+ mParent.setLocale(locale, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the rendering target should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideTarget(boolean override) {
+ mOverride |= CFG_TARGET;
+ }
+
+ /**
+ * Returns true if the target is overridden
+ *
+ * @return true if the target is overridden
+ */
+ public final boolean isOverridingTarget() {
+ return (mOverride & CFG_TARGET) != 0;
+ }
+
+ @Override
+ @Nullable
+ public IAndroidTarget getTarget() {
+ if (isOverridingTarget()) {
+ return super.getTarget();
+ } else {
+ return mParent.getTarget();
+ }
+ }
+
+ @Override
+ public void setTarget(IAndroidTarget target, boolean skipSync) {
+ if (isOverridingTarget()) {
+ super.setTarget(target, skipSync);
+ } else {
+ mParent.setTarget(target, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the device should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideDevice(boolean override) {
+ mOverride |= CFG_DEVICE;
+ }
+
+ /**
+ * Returns true if the device is overridden
+ *
+ * @return true if the device is overridden
+ */
+ public final boolean isOverridingDevice() {
+ return (mOverride & CFG_DEVICE) != 0;
+ }
+
+ @Override
+ @Nullable
+ public Device getDevice() {
+ if (isOverridingDevice()) {
+ return super.getDevice();
+ } else {
+ return mParent.getDevice();
+ }
+ }
+
+ @Override
+ public void setDevice(Device device, boolean skipSync) {
+ if (isOverridingDevice()) {
+ super.setDevice(device, skipSync);
+ } else {
+ mParent.setDevice(device, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the device state should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideDeviceState(boolean override) {
+ mOverride |= CFG_DEVICE_STATE;
+ }
+
+ /**
+ * Returns true if the device state is overridden
+ *
+ * @return true if the device state is overridden
+ */
+ public final boolean isOverridingDeviceState() {
+ return (mOverride & CFG_DEVICE_STATE) != 0;
+ }
+
+ @Override
+ @Nullable
+ public State getDeviceState() {
+ if (isOverridingDeviceState()) {
+ return super.getDeviceState();
+ } else {
+ State state = mParent.getDeviceState();
+ if (isOverridingDevice()) {
+ // If the device differs, I need to look up a suitable equivalent state
+ // on our device
+ if (state != null) {
+ Device device = super.getDevice();
+ if (device != null) {
+ return device.getState(state.getName());
+ }
+ }
+ }
+
+ return state;
+ }
+ }
+
+ @Override
+ public void setDeviceState(State state, boolean skipSync) {
+ if (isOverridingDeviceState()) {
+ super.setDeviceState(state, skipSync);
+ } else {
+ if (isOverridingDevice()) {
+ Device device = super.getDevice();
+ if (device != null) {
+ State equivalentState = device.getState(state.getName());
+ if (equivalentState != null) {
+ state = equivalentState;
+ }
+ }
+ }
+ mParent.setDeviceState(state, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the night mode should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideNightMode(boolean override) {
+ mOverride |= CFG_NIGHT_MODE;
+ }
+
+ /**
+ * Returns true if the night mode is overridden
+ *
+ * @return true if the night mode is overridden
+ */
+ public final boolean isOverridingNightMode() {
+ return (mOverride & CFG_NIGHT_MODE) != 0;
+ }
+
+ @Override
+ @NonNull
+ public NightMode getNightMode() {
+ if (isOverridingNightMode()) {
+ return super.getNightMode();
+ } else {
+ return mParent.getNightMode();
+ }
+ }
+
+ @Override
+ public void setNightMode(@NonNull NightMode night, boolean skipSync) {
+ if (isOverridingNightMode()) {
+ super.setNightMode(night, skipSync);
+ } else {
+ mParent.setNightMode(night, skipSync);
+ }
+ }
+
+ /**
+ * Sets whether the UI mode should be overridden by this configuration
+ *
+ * @param override if true, override the inherited value
+ */
+ public void setOverrideUiMode(boolean override) {
+ mOverride |= CFG_UI_MODE;
+ }
+
+ /**
+ * Returns true if the UI mode is overridden
+ *
+ * @return true if the UI mode is overridden
+ */
+ public final boolean isOverridingUiMode() {
+ return (mOverride & CFG_UI_MODE) != 0;
+ }
+
+ @Override
+ @NonNull
+ public UiMode getUiMode() {
+ if (isOverridingUiMode()) {
+ return super.getUiMode();
+ } else {
+ return mParent.getUiMode();
+ }
+ }
+
+ @Override
+ public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) {
+ if (isOverridingUiMode()) {
+ super.setUiMode(uiMode, skipSync);
+ } else {
+ mParent.setUiMode(uiMode, skipSync);
+ }
+ }
+
+ /**
+ * Returns the configuration this {@linkplain NestedConfiguration} is
+ * inheriting from
+ *
+ * @return the configuration this configuration is inheriting from
+ */
+ @NonNull
+ public Configuration getParent() {
+ return mParent;
+ }
+
+ @Override
+ @Nullable
+ public String getActivity() {
+ return mParent.getActivity();
+ }
+
+ @Override
+ public void setActivity(String activity) {
+ super.setActivity(activity);
+ }
+
+ /**
+ * Returns a computed display name (ignoring the value stored by
+ * {@link #setDisplayName(String)}) by looking at the override flags
+ * and picking a suitable name.
+ *
+ * @return a suitable display name
+ */
+ @Nullable
+ public String computeDisplayName() {
+ return computeDisplayName(mOverride, this);
+ }
+
+ /**
+ * Computes a display name for the given configuration, using the given
+ * override flags (which correspond to the {@code CFG_} constants in
+ * {@link ConfigurationClient}
+ *
+ * @param flags the override bitmask
+ * @param configuration the configuration to fetch values from
+ * @return a suitable display name
+ */
+ @Nullable
+ public static String computeDisplayName(int flags, @NonNull Configuration configuration) {
+ if ((flags & CFG_LOCALE) != 0) {
+ return ConfigurationChooser.getLocaleLabel(configuration.mConfigChooser,
+ configuration.getLocale(), false);
+ }
+
+ if ((flags & CFG_TARGET) != 0) {
+ return ConfigurationChooser.getRenderingTargetLabel(configuration.getTarget(), false);
+ }
+
+ if ((flags & CFG_DEVICE) != 0) {
+ return ConfigurationChooser.getDeviceLabel(configuration.getDevice(), true);
+ }
+
+ if ((flags & CFG_DEVICE_STATE) != 0) {
+ State deviceState = configuration.getDeviceState();
+ if (deviceState != null) {
+ return deviceState.getName();
+ }
+ }
+
+ if ((flags & CFG_NIGHT_MODE) != 0) {
+ return configuration.getNightMode().getLongDisplayValue();
+ }
+
+ if ((flags & CFG_UI_MODE) != 0) {
+ configuration.getUiMode().getLongDisplayValue();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this.getClass())
+ .add("parent", mParent.getDisplayName()) //$NON-NLS-1$
+ .add("display", getDisplayName()) //$NON-NLS-1$
+ .add("overrideLocale", isOverridingLocale()) //$NON-NLS-1$
+ .add("overrideTarget", isOverridingTarget()) //$NON-NLS-1$
+ .add("overrideDevice", isOverridingDevice()) //$NON-NLS-1$
+ .add("overrideDeviceState", isOverridingDeviceState()) //$NON-NLS-1$
+ .add("persistent", toPersistentString()) //$NON-NLS-1$
+ .toString();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java
new file mode 100644
index 000000000..5cad29afc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/OrientationMenuAction.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SubmenuAction;
+import com.android.resources.NightMode;
+import com.android.resources.ScreenOrientation;
+import com.android.resources.UiMode;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.List;
+
+/**
+ * Action which creates a submenu that shows the available orientations as well
+ * as some related options for night mode and dock mode
+ */
+class OrientationMenuAction extends SubmenuAction {
+ // Constants used to indicate what type of menu is being shown, such that
+ // the submenus can lazily construct their contents
+ private static final int MENU_NIGHTMODE = 1;
+ private static final int MENU_UIMODE = 2;
+
+ private final ConfigurationChooser mConfigChooser;
+ /** Type of menu; one of the constants {@link #MENU_NIGHTMODE} etc */
+ private final int mType;
+
+ OrientationMenuAction(int type, String title, ConfigurationChooser configuration) {
+ super(title);
+ mType = type;
+ mConfigChooser = configuration;
+ }
+
+ static void showMenu(ConfigurationChooser configChooser, ToolItem combo) {
+ MenuManager manager = new MenuManager();
+
+ // Show toggles for all the available states
+
+ Configuration configuration = configChooser.getConfiguration();
+ Device device = configuration.getDevice();
+ State current = configuration.getDeviceState();
+ if (device != null) {
+ List<State> states = device.getAllStates();
+
+ if (states.size() > 1 && current != null) {
+ State flip = configuration.getNextDeviceState(current);
+ String flipName = flip != null ? flip.getName() : current.getName();
+ manager.add(new DeviceConfigAction(configChooser,
+ String.format("Switch to %1$s", flipName), flip, false, true));
+ manager.add(new Separator());
+ }
+
+ for (State config : states) {
+ manager.add(new DeviceConfigAction(configChooser, config.getName(),
+ config, config == current, false));
+ }
+ manager.add(new Separator());
+ }
+ manager.add(new OrientationMenuAction(MENU_UIMODE, "UI Mode", configChooser));
+ manager.add(new Separator());
+ manager.add(new OrientationMenuAction(MENU_NIGHTMODE, "Night Mode", configChooser));
+
+ Menu menu = manager.createContextMenu(configChooser.getShell());
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ switch (mType) {
+ case MENU_NIGHTMODE: {
+ NightMode selected = mConfigChooser.getConfiguration().getNightMode();
+ for (NightMode mode : NightMode.values()) {
+ boolean checked = mode == selected;
+ SelectNightModeAction action = new SelectNightModeAction(mode, checked);
+ new ActionContributionItem(action).fill(menu, -1);
+
+ }
+ break;
+ }
+ case MENU_UIMODE: {
+ UiMode selected = mConfigChooser.getConfiguration().getUiMode();
+ for (UiMode mode : UiMode.values()) {
+ boolean checked = mode == selected;
+ SelectUiModeAction action = new SelectUiModeAction(mode, checked);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ break;
+ }
+ }
+ }
+
+
+ private class SelectNightModeAction extends Action {
+ private final NightMode mMode;
+
+ private SelectNightModeAction(NightMode mode, boolean checked) {
+ super(mode.getLongDisplayValue(), IAction.AS_RADIO_BUTTON);
+ mMode = mode;
+ if (checked) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ configuration.setNightMode(mMode, false);
+ mConfigChooser.notifyFolderConfigChanged();
+ }
+ }
+
+ private class SelectUiModeAction extends Action {
+ private final UiMode mMode;
+
+ private SelectUiModeAction(UiMode mode, boolean checked) {
+ super(mode.getLongDisplayValue(), IAction.AS_RADIO_BUTTON);
+ mMode = mode;
+ if (checked) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ configuration.setUiMode(mMode, false);
+ }
+ }
+
+ private static class DeviceConfigAction extends Action {
+ private final ConfigurationChooser mConfiguration;
+ private final State mState;
+
+ private DeviceConfigAction(ConfigurationChooser configuration, String title,
+ State state, boolean checked, boolean flip) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mConfiguration = configuration;
+ mState = state;
+ if (checked) {
+ setChecked(true);
+ }
+ ScreenOrientation orientation = configuration.getOrientation(state);
+ setImageDescriptor(configuration.getOrientationImage(orientation, flip));
+ }
+
+ @Override
+ public void run() {
+ mConfiguration.selectDeviceState(mState);
+ mConfiguration.onDeviceConfigChange();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java
new file mode 100644
index 000000000..d062849d1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/SelectThemeAction.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+
+/**
+ * Action which brings up the "Create new XML File" wizard, pre-selected with the
+ * animation category
+ */
+class SelectThemeAction extends Action {
+ private final ConfigurationChooser mConfiguration;
+ private final String mTheme;
+
+ public SelectThemeAction(ConfigurationChooser configuration, String title, String theme,
+ boolean selected) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ assert theme.startsWith(STYLE_RESOURCE_PREFIX)
+ || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme;
+ mConfiguration = configuration;
+ mTheme = theme;
+ if (selected) {
+ setChecked(selected);
+ }
+ }
+
+ @Override
+ public void run() {
+ mConfiguration.selectTheme(mTheme);
+ mConfiguration.onThemeChange();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java
new file mode 100644
index 000000000..71905f7c9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.List;
+import java.util.RandomAccess;
+
+/**
+ * The {@linkplain TargetMenuListener} class is responsible for
+ * generating the rendering target menu in the {@link ConfigurationChooser}.
+ */
+class TargetMenuListener extends SelectionAdapter {
+ private final ConfigurationChooser mConfigChooser;
+ private final IAndroidTarget mTarget;
+ private final boolean mPickBest;
+
+ TargetMenuListener(
+ @NonNull ConfigurationChooser configChooser,
+ @Nullable IAndroidTarget target,
+ boolean pickBest) {
+ mConfigChooser = configChooser;
+ mTarget = target;
+ mPickBest = pickBest;
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IAndroidTarget target = mTarget;
+ AdtPrefs prefs = AdtPrefs.getPrefs();
+ if (mPickBest) {
+ boolean autoPick = prefs.isAutoPickRenderTarget();
+ autoPick = !autoPick;
+ prefs.setAutoPickRenderTarget(autoPick);
+ if (autoPick) {
+ target = ConfigurationMatcher.findDefaultRenderTarget(mConfigChooser);
+ } else {
+ // Turn it off, but keep current target until another one is chosen
+ return;
+ }
+ } else {
+ // Manually picked some other target: turn off auto-pick
+ prefs.setAutoPickRenderTarget(false);
+ }
+ mConfigChooser.selectTarget(target);
+ mConfigChooser.onRenderingTargetChange();
+ }
+
+ static void show(ConfigurationChooser chooser, ToolItem combo) {
+ Menu menu = new Menu(chooser.getShell(), SWT.POP_UP);
+ Configuration configuration = chooser.getConfiguration();
+ IAndroidTarget current = configuration.getTarget();
+ List<IAndroidTarget> targets = chooser.getTargetList();
+ boolean haveRecent = false;
+
+ MenuItem menuItem = new MenuItem(menu, SWT.CHECK);
+ menuItem.setText("Automatically Pick Best");
+ menuItem.addSelectionListener(new TargetMenuListener(chooser, null, true));
+ if (AdtPrefs.getPrefs().isAutoPickRenderTarget()) {
+ menuItem.setSelection(true);
+ }
+
+ @SuppressWarnings("unused")
+ MenuItem separator = new MenuItem(menu, SWT.SEPARATOR);
+
+ // Process in reverse order: most important targets first
+ assert targets instanceof RandomAccess;
+ for (int i = targets.size() - 1; i >= 0; i--) {
+ IAndroidTarget target = targets.get(i);
+
+ AndroidVersion version = target.getVersion();
+ if (version.getApiLevel() >= 7) {
+ haveRecent = true;
+ } else if (haveRecent) {
+ // Don't show ancient rendering targets; they're pretty broken
+ // (unless of course all you have are ancient targets)
+ break;
+ }
+
+ String title = ConfigurationChooser.getRenderingTargetLabel(target, false);
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+
+ boolean selected = current == target;
+ if (selected) {
+ item.setSelection(true);
+ }
+
+ item.addSelectionListener(new TargetMenuListener(chooser, target, false));
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java
new file mode 100644
index 000000000..b1ce21d36
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+
+import com.android.ide.eclipse.adt.internal.editors.Hyperlinks;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SubmenuAction;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Action which creates a submenu displaying available themes
+ */
+class ThemeMenuAction extends SubmenuAction {
+ private static final String DEVICE_LIGHT_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.DeviceDefault.Light"; //$NON-NLS-1$
+ private static final String HOLO_LIGHT_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo.Light"; //$NON-NLS-1$
+ private static final String DEVICE_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.DeviceDefault"; //$NON-NLS-1$
+ private static final String HOLO_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$
+ private static final String LIGHT_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX +"Theme.Light"; //$NON-NLS-1$
+ private static final String THEME_PREFIX =
+ ANDROID_STYLE_RESOURCE_PREFIX +"Theme"; //$NON-NLS-1$
+
+ // Constants used to indicate what type of menu is being shown, such that
+ // the submenus can lazily construct their contents
+ private static final int MENU_MANIFEST = 1;
+ private static final int MENU_PROJECT = 2;
+ private static final int MENU_THEME = 3;
+ private static final int MENU_THEME_LIGHT = 4;
+ private static final int MENU_HOLO = 5;
+ private static final int MENU_HOLO_LIGHT = 6;
+ private static final int MENU_DEVICE = 7;
+ private static final int MENU_DEVICE_LIGHT = 8;
+ private static final int MENU_ALL = 9;
+
+ private final ConfigurationChooser mConfigChooser;
+ private final List<String> mThemeList;
+ /** Type of menu; one of the constants {@link #MENU_ALL} etc */
+ private final int mType;
+
+ ThemeMenuAction(int type, String title, ConfigurationChooser configuration,
+ List<String> themeList) {
+ super(title);
+ mType = type;
+ mConfigChooser = configuration;
+ mThemeList = themeList;
+ }
+
+ static void showThemeMenu(ConfigurationChooser configChooser, ToolItem combo,
+ List<String> themeList) {
+ MenuManager manager = new MenuManager();
+
+ // First show the currently selected theme (grayed out since you can't
+ // reselect it)
+ Configuration configuration = configChooser.getConfiguration();
+ String currentTheme = configuration.getTheme();
+ String currentName = null;
+ if (currentTheme != null) {
+ currentName = ResourceHelper.styleToTheme(currentTheme);
+ SelectThemeAction action = new SelectThemeAction(configChooser,
+ currentName,
+ currentTheme,
+ true /* selected */);
+ action.setEnabled(false);
+ manager.add(action);
+ manager.add(new Separator());
+ }
+
+ String preferred = configuration.computePreferredTheme();
+ if (preferred != null && !preferred.equals(currentTheme)) {
+ manager.add(new SelectThemeAction(configChooser,
+ ResourceHelper.styleToTheme(preferred),
+ preferred, false /* selected */));
+ manager.add(new Separator());
+ }
+
+ IAndroidTarget target = configuration.getTarget();
+ int apiLevel = target != null ? target.getVersion().getApiLevel() : 1;
+ boolean hasHolo = apiLevel >= 11; // Honeycomb
+ boolean hasDeviceDefault = apiLevel >= 14; // ICS
+
+ // TODO: Add variations of the current theme here, e.g.
+ // if you're using Theme.Holo, add Theme.Holo.Dialog, Theme.Holo.Panel,
+ // Theme.Holo.Wallpaper etc
+
+ manager.add(new ThemeMenuAction(MENU_PROJECT, "Project Themes",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_MANIFEST, "Manifest Themes",
+ configChooser, themeList));
+
+ manager.add(new Separator());
+
+ if (hasHolo) {
+ manager.add(new ThemeMenuAction(MENU_HOLO, "Holo",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_HOLO_LIGHT, "Holo.Light",
+ configChooser, themeList));
+ }
+ if (hasDeviceDefault) {
+ manager.add(new ThemeMenuAction(MENU_DEVICE, "DeviceDefault",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_DEVICE_LIGHT, "DeviceDefault.Light",
+ configChooser, themeList));
+ }
+ manager.add(new ThemeMenuAction(MENU_THEME, "Theme",
+ configChooser, themeList));
+ manager.add(new ThemeMenuAction(MENU_THEME_LIGHT, "Theme.Light",
+ configChooser, themeList));
+
+ // TODO: Add generic types like Wallpaper, Dialog, Alert, etc here, with
+ // submenus for picking it within each theme category?
+
+ manager.add(new Separator());
+ manager.add(new ThemeMenuAction(MENU_ALL, "All",
+ configChooser, themeList));
+
+ if (currentTheme != null) {
+ assert currentName != null;
+ manager.add(new Separator());
+ String title = String.format("Open %1$s Declaration...", currentName);
+ manager.add(new OpenThemeAction(title, configChooser.getEditedFile(), currentTheme));
+ }
+
+ Menu menu = manager.createContextMenu(configChooser.getShell());
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ switch (mType) {
+ case MENU_ALL:
+ addMenuItems(menu, mThemeList);
+ break;
+
+ case MENU_MANIFEST: {
+ IProject project = mConfigChooser.getEditedFile().getProject();
+ ManifestInfo manifest = ManifestInfo.get(project);
+ Configuration configuration = mConfigChooser.getConfiguration();
+ String activity = configuration.getActivity();
+ if (activity != null) {
+ ActivityAttributes attributes = manifest.getActivityAttributes(activity);
+ if (attributes != null) {
+ String theme = attributes.getTheme();
+ if (theme != null) {
+ addMenuItem(menu, theme, isSelectedTheme(theme));
+ }
+ }
+ }
+
+ String manifestTheme = manifest.getManifestTheme();
+ boolean found = false;
+ Set<String> allThemes = new HashSet<String>();
+ if (manifestTheme != null) {
+ found = true;
+ allThemes.add(manifestTheme);
+ }
+ for (ActivityAttributes info : manifest.getActivityAttributesMap().values()) {
+ if (info.getTheme() != null) {
+ found = true;
+ allThemes.add(info.getTheme());
+ }
+ }
+ List<String> sorted = new ArrayList<String>(allThemes);
+ Collections.sort(sorted);
+ String current = configuration.getTheme();
+ for (String theme : sorted) {
+ boolean selected = theme.equals(current);
+ addMenuItem(menu, theme, selected);
+ }
+ if (!found) {
+ addDisabledMessageItem("No themes are registered in the manifest");
+ }
+ break;
+ }
+ case MENU_PROJECT: {
+ int size = mThemeList.size();
+ List<String> themes = new ArrayList<String>(size);
+ for (int i = 0; i < size; i++) {
+ String theme = mThemeList.get(i);
+ if (ResourceHelper.isProjectStyle(theme)) {
+ themes.add(theme);
+ }
+ }
+ if (themes.isEmpty()) {
+ addDisabledMessageItem("There are no local theme styles in the project");
+ } else {
+ addMenuItems(menu, themes);
+ }
+ break;
+ }
+ case MENU_THEME: {
+ // Can't just use the usual filterThemes() call here because we need
+ // to exclude on multiple prefixes: Holo, DeviceDefault, Light, ...
+ List<String> themes = new ArrayList<String>(mThemeList.size());
+ for (String theme : mThemeList) {
+ if (theme.startsWith(THEME_PREFIX)
+ && !theme.startsWith(LIGHT_PREFIX)
+ && !theme.startsWith(HOLO_PREFIX)
+ && !theme.startsWith(DEVICE_PREFIX)) {
+ themes.add(theme);
+ }
+ }
+
+ addMenuItems(menu, themes);
+ break;
+ }
+ case MENU_THEME_LIGHT:
+ addMenuItems(menu, filterThemes(LIGHT_PREFIX, null));
+ break;
+ case MENU_HOLO:
+ addMenuItems(menu, filterThemes(HOLO_PREFIX, HOLO_LIGHT_PREFIX));
+ break;
+ case MENU_HOLO_LIGHT:
+ addMenuItems(menu, filterThemes(HOLO_LIGHT_PREFIX, null));
+ break;
+ case MENU_DEVICE:
+ addMenuItems(menu, filterThemes(DEVICE_PREFIX, DEVICE_LIGHT_PREFIX));
+ break;
+ case MENU_DEVICE_LIGHT:
+ addMenuItems(menu, filterThemes(DEVICE_LIGHT_PREFIX, null));
+ break;
+ }
+ }
+
+ private List<String> filterThemes(String include, String exclude) {
+ List<String> themes = new ArrayList<String>(mThemeList.size());
+ for (String theme : mThemeList) {
+ if (theme.startsWith(include) && (exclude == null || !theme.startsWith(exclude))) {
+ themes.add(theme);
+ }
+ }
+
+ return themes;
+ }
+
+ private void addMenuItems(Menu menu, List<String> themes) {
+ String current = mConfigChooser.getConfiguration().getTheme();
+ for (String theme : themes) {
+ addMenuItem(menu, theme, theme.equals(current));
+ }
+ }
+
+ private boolean isSelectedTheme(String theme) {
+ return theme.equals(mConfigChooser.getConfiguration().getTheme());
+ }
+
+ private void addMenuItem(Menu menu, String theme, boolean selected) {
+ String title = ResourceHelper.styleToTheme(theme);
+ SelectThemeAction action = new SelectThemeAction(mConfigChooser, title, theme, selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+
+ private static class OpenThemeAction extends Action {
+ private final String mTheme;
+ private final IFile mFile;
+
+ private OpenThemeAction(String title, IFile file, String theme) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mFile = file;
+ mTheme = theme;
+ }
+
+ @Override
+ public void run() {
+ IProject project = mFile.getProject();
+ IHyperlink[] links = Hyperlinks.getResourceLinks(null, mTheme, project, null);
+ if (links != null && links.length > 0) {
+ IHyperlink link = links[0];
+ link.open();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java
new file mode 100644
index 000000000..f472cd6b3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.resources.Density;
+import com.android.resources.NightMode;
+import com.android.resources.UiMode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Hardware;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.State;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An {@linkplain VaryingConfiguration} is a {@link Configuration} which
+ * inherits all of its values from a different configuration, except for one or
+ * more attributes where it overrides a custom value, and the overridden value
+ * will always <b>differ</b> from the inherited value!
+ * <p>
+ * For example, a {@linkplain VaryingConfiguration} may state that it
+ * overrides the locale, and if the inherited locale is "en", then the returned
+ * locale from the {@linkplain VaryingConfiguration} may be for example "nb",
+ * but never "en".
+ * <p>
+ * The configuration will attempt to make its changed inherited value to be as
+ * different as possible from the inherited value. Thus, a configuration which
+ * overrides the device will probably return a phone-sized screen if the
+ * inherited device is a tablet, or vice versa.
+ */
+public class VaryingConfiguration extends NestedConfiguration {
+ /** Variation version; see {@link #setVariation(int)} */
+ private int mVariation;
+
+ /** Variation version count; see {@link #setVariationCount(int)} */
+ private int mVariationCount;
+
+ /** Bitmask of attributes to be varied/alternated from the parent */
+ private int mAlternate;
+
+ /**
+ * Constructs a new {@linkplain VaryingConfiguration}.
+ * Construct via
+ *
+ * @param chooser the associated chooser
+ * @param configuration the configuration to inherit from
+ */
+ private VaryingConfiguration(
+ @NonNull ConfigurationChooser chooser,
+ @NonNull Configuration configuration) {
+ super(chooser, configuration);
+ }
+
+ /**
+ * Creates a new {@linkplain Configuration} which inherits values from the
+ * given parent {@linkplain Configuration}, possibly overriding some as
+ * well.
+ *
+ * @param chooser the associated chooser
+ * @param parent the configuration to inherit values from
+ * @return a new configuration
+ */
+ @NonNull
+ public static VaryingConfiguration create(@NonNull ConfigurationChooser chooser,
+ @NonNull Configuration parent) {
+ return new VaryingConfiguration(chooser, parent);
+ }
+
+ /**
+ * Creates a new {@linkplain VaryingConfiguration} that has the same overriding
+ * attributes as the given other {@linkplain VaryingConfiguration}.
+ *
+ * @param other the configuration to copy overrides from
+ * @param parent the parent to tie the configuration to for inheriting values
+ * @return a new configuration
+ */
+ @NonNull
+ public static VaryingConfiguration create(
+ @NonNull VaryingConfiguration other,
+ @NonNull Configuration parent) {
+ VaryingConfiguration configuration =
+ new VaryingConfiguration(other.mConfigChooser, parent);
+ initFrom(configuration, other, other, false);
+ configuration.mAlternate = other.mAlternate;
+ configuration.mVariation = other.mVariation;
+ configuration.mVariationCount = other.mVariationCount;
+ configuration.syncFolderConfig();
+
+ return configuration;
+ }
+
+ /**
+ * Returns the alternate flags for this configuration. Corresponds to
+ * the {@code CFG_} flags in {@link ConfigurationClient}.
+ *
+ * @return the bitmask
+ */
+ public int getAlternateFlags() {
+ return mAlternate;
+ }
+
+ @Override
+ public void syncFolderConfig() {
+ super.syncFolderConfig();
+ updateDisplayName();
+ }
+
+ /**
+ * Sets the variation version for this
+ * {@linkplain VaryingConfiguration}. There might be multiple
+ * {@linkplain VaryingConfiguration} instances inheriting from a
+ * {@link Configuration}. The variation version allows them to choose
+ * different complementing values, so they don't all flip to the same other
+ * (out of multiple choices) value. The {@link #setVariationCount(int)}
+ * value can be used to determine how to partition the buckets of values.
+ * Also updates the variation count if necessary.
+ *
+ * @param variation variation version
+ */
+ public void setVariation(int variation) {
+ mVariation = variation;
+ mVariationCount = Math.max(mVariationCount, variation + 1);
+ }
+
+ /**
+ * Sets the number of {@link VaryingConfiguration} variations mapped
+ * to the same parent configuration as this one. See
+ * {@link #setVariation(int)} for details.
+ *
+ * @param count the total number of variation versions
+ */
+ public void setVariationCount(int count) {
+ mVariationCount = count;
+ }
+
+ /**
+ * Updates the display name in this configuration based on the values and override settings
+ */
+ public void updateDisplayName() {
+ setDisplayName(computeDisplayName());
+ }
+
+ @Override
+ @NonNull
+ public Locale getLocale() {
+ if (isOverridingLocale()) {
+ return super.getLocale();
+ }
+ Locale locale = mParent.getLocale();
+ if (isAlternatingLocale() && locale != null) {
+ List<Locale> locales = mConfigChooser.getLocaleList();
+ for (Locale l : locales) {
+ // TODO: Try to be smarter about which one we pick; for example, try
+ // to pick a language that is substantially different from the inherited
+ // language, such as either with the strings of the largest or shortest
+ // length, or perhaps based on some geography or population metrics
+ if (!l.equals(locale)) {
+ locale = l;
+ break;
+ }
+ }
+ }
+
+ return locale;
+ }
+
+ @Override
+ @Nullable
+ public IAndroidTarget getTarget() {
+ if (isOverridingTarget()) {
+ return super.getTarget();
+ }
+ IAndroidTarget target = mParent.getTarget();
+ if (isAlternatingTarget() && target != null) {
+ List<IAndroidTarget> targets = mConfigChooser.getTargetList();
+ if (!targets.isEmpty()) {
+ // Pick a different target: if you're showing the most recent render target,
+ // then pick the lowest supported target, and vice versa
+ IAndroidTarget mostRecent = targets.get(targets.size() - 1);
+ if (target.equals(mostRecent)) {
+ // Find oldest supported
+ ManifestInfo info = ManifestInfo.get(mConfigChooser.getProject());
+ int minSdkVersion = info.getMinSdkVersion();
+ for (IAndroidTarget t : targets) {
+ if (t.getVersion().getApiLevel() >= minSdkVersion) {
+ target = t;
+ break;
+ }
+ }
+ } else {
+ target = mostRecent;
+ }
+ }
+ }
+
+ return target;
+ }
+
+ // Cached values, key=parent's device, cached value=device
+ private Device mPrevParentDevice;
+ private Device mPrevDevice;
+
+ @Override
+ @Nullable
+ public Device getDevice() {
+ if (isOverridingDevice()) {
+ return super.getDevice();
+ }
+ Device device = mParent.getDevice();
+ if (isAlternatingDevice() && device != null) {
+ if (device == mPrevParentDevice) {
+ return mPrevDevice;
+ }
+
+ mPrevParentDevice = device;
+
+ // Pick a different device
+ Collection<Device> devices = mConfigChooser.getDevices();
+
+ // Divide up the available devices into {@link #mVariationCount} + 1 buckets
+ // (the + 1 is for the bucket now taken up by the inherited value).
+ // Then assign buckets to each {@link #mVariation} version, and pick one
+ // from the bucket assigned to this current configuration's variation version.
+
+ // I could just divide up the device list count, but that would treat a lot of
+ // very similar phones as having the same kind of variety as the 7" and 10"
+ // tablets which are sitting right next to each other in the device list.
+ // Instead, do this by screen size.
+
+
+ double smallest = 100;
+ double biggest = 1;
+ for (Device d : devices) {
+ double size = getScreenSize(d);
+ if (size < 0) {
+ continue; // no data
+ }
+ if (size >= biggest) {
+ biggest = size;
+ }
+ if (size <= smallest) {
+ smallest = size;
+ }
+ }
+
+ int bucketCount = mVariationCount + 1;
+ double inchesPerBucket = (biggest - smallest) / bucketCount;
+
+ double overriddenSize = getScreenSize(device);
+ int overriddenBucket = (int) ((overriddenSize - smallest) / inchesPerBucket);
+ int bucket = (mVariation < overriddenBucket) ? mVariation : mVariation + 1;
+ double from = inchesPerBucket * bucket + smallest;
+ double to = from + inchesPerBucket;
+ if (biggest - to < 0.1) {
+ to = biggest + 0.1;
+ }
+
+ boolean canScaleNinePatch = supports(Capability.FIXED_SCALABLE_NINE_PATCH);
+ for (Device d : devices) {
+ double size = getScreenSize(d);
+ if (size >= from && size < to) {
+ if (!canScaleNinePatch) {
+ Density density = getDensity(d);
+ if (density == Density.TV || density == Density.LOW) {
+ continue;
+ }
+ }
+
+ device = d;
+ break;
+ }
+ }
+
+ mPrevDevice = device;
+ }
+
+ return device;
+ }
+
+ /**
+ * Returns the density of the given device
+ *
+ * @param device the device to check
+ * @return the density or null
+ */
+ @Nullable
+ private static Density getDensity(@NonNull Device device) {
+ Hardware hardware = device.getDefaultHardware();
+ if (hardware != null) {
+ Screen screen = hardware.getScreen();
+ if (screen != null) {
+ return screen.getPixelDensity();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the diagonal length of the given device
+ *
+ * @param device the device to check
+ * @return the diagonal length or -1
+ */
+ private static double getScreenSize(@NonNull Device device) {
+ Hardware hardware = device.getDefaultHardware();
+ if (hardware != null) {
+ Screen screen = hardware.getScreen();
+ if (screen != null) {
+ return screen.getDiagonalLength();
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ @Nullable
+ public State getDeviceState() {
+ if (isOverridingDeviceState()) {
+ return super.getDeviceState();
+ }
+ State state = mParent.getDeviceState();
+ if (isAlternatingDeviceState() && state != null) {
+ State alternate = getNextDeviceState(state);
+
+ return alternate;
+ } else {
+ if ((isAlternatingDevice() || isOverridingDevice()) && state != null) {
+ // If the device differs, I need to look up a suitable equivalent state
+ // on our device
+ Device device = getDevice();
+ if (device != null) {
+ return device.getState(state.getName());
+ }
+ }
+
+ return state;
+ }
+ }
+
+ @Override
+ @NonNull
+ public NightMode getNightMode() {
+ if (isOverridingNightMode()) {
+ return super.getNightMode();
+ }
+ NightMode nightMode = mParent.getNightMode();
+ if (isAlternatingNightMode() && nightMode != null) {
+ nightMode = nightMode == NightMode.NIGHT ? NightMode.NOTNIGHT : NightMode.NIGHT;
+ return nightMode;
+ } else {
+ return nightMode;
+ }
+ }
+
+ @Override
+ @NonNull
+ public UiMode getUiMode() {
+ if (isOverridingUiMode()) {
+ return super.getUiMode();
+ }
+ UiMode uiMode = mParent.getUiMode();
+ if (isAlternatingUiMode() && uiMode != null) {
+ // TODO: Use manifest's supports screen to decide which are most relevant
+ // (as well as which available configuration qualifiers are present in the
+ // layout)
+ UiMode[] values = UiMode.values();
+ uiMode = values[(uiMode.ordinal() + 1) % values.length];
+ return uiMode;
+ } else {
+ return uiMode;
+ }
+ }
+
+ @Override
+ @Nullable
+ public String computeDisplayName() {
+ return computeDisplayName(getOverrideFlags() | mAlternate, this);
+ }
+
+ /**
+ * Sets whether the locale should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateLocale(boolean alternate) {
+ mAlternate |= CFG_LOCALE;
+ }
+
+ /**
+ * Returns true if the locale is alternated
+ *
+ * @return true if the locale is alternated
+ */
+ public final boolean isAlternatingLocale() {
+ return (mAlternate & CFG_LOCALE) != 0;
+ }
+
+ /**
+ * Sets whether the rendering target should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateTarget(boolean alternate) {
+ mAlternate |= CFG_TARGET;
+ }
+
+ /**
+ * Returns true if the target is alternated
+ *
+ * @return true if the target is alternated
+ */
+ public final boolean isAlternatingTarget() {
+ return (mAlternate & CFG_TARGET) != 0;
+ }
+
+ /**
+ * Sets whether the device should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateDevice(boolean alternate) {
+ mAlternate |= CFG_DEVICE;
+ }
+
+ /**
+ * Returns true if the device is alternated
+ *
+ * @return true if the device is alternated
+ */
+ public final boolean isAlternatingDevice() {
+ return (mAlternate & CFG_DEVICE) != 0;
+ }
+
+ /**
+ * Sets whether the device state should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateDeviceState(boolean alternate) {
+ mAlternate |= CFG_DEVICE_STATE;
+ }
+
+ /**
+ * Returns true if the device state is alternated
+ *
+ * @return true if the device state is alternated
+ */
+ public final boolean isAlternatingDeviceState() {
+ return (mAlternate & CFG_DEVICE_STATE) != 0;
+ }
+
+ /**
+ * Sets whether the night mode should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateNightMode(boolean alternate) {
+ mAlternate |= CFG_NIGHT_MODE;
+ }
+
+ /**
+ * Returns true if the night mode is alternated
+ *
+ * @return true if the night mode is alternated
+ */
+ public final boolean isAlternatingNightMode() {
+ return (mAlternate & CFG_NIGHT_MODE) != 0;
+ }
+
+ /**
+ * Sets whether the UI mode should be alternated by this configuration
+ *
+ * @param alternate if true, alternate the inherited value
+ */
+ public void setAlternateUiMode(boolean alternate) {
+ mAlternate |= CFG_UI_MODE;
+ }
+
+ /**
+ * Returns true if the UI mode is alternated
+ *
+ * @return true if the UI mode is alternated
+ */
+ public final boolean isAlternatingUiMode() {
+ return (mAlternate & CFG_UI_MODE) != 0;
+ }
+
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java
new file mode 100644
index 000000000..6df6929a7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java
@@ -0,0 +1,621 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.descriptors;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.AUTO_URI;
+import static com.android.SdkConstants.CLASS_VIEWGROUP;
+import static com.android.SdkConstants.URI_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.AttrsXmlParser;
+import com.android.ide.common.resources.platform.ViewClassInfo;
+import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.google.common.collect.Maps;
+import com.google.common.collect.ObjectArrays;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IClassFile;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Service responsible for creating/managing {@link ViewElementDescriptor} objects for custom
+ * View classes per project.
+ * <p/>
+ * The service provides an on-demand monitoring of custom classes to check for changes. Monitoring
+ * starts once a request for an {@link ViewElementDescriptor} object has been done for a specific
+ * class.
+ * <p/>
+ * The monitoring will notify a listener of any changes in the class triggering a change in its
+ * associated {@link ViewElementDescriptor} object.
+ * <p/>
+ * If the custom class does not exist, no monitoring is put in place to avoid having to listen
+ * to all class changes in the projects.
+ */
+public final class CustomViewDescriptorService {
+
+ private static CustomViewDescriptorService sThis = new CustomViewDescriptorService();
+
+ /**
+ * Map where keys are the project, and values are another map containing all the known
+ * custom View class for this project. The custom View class are stored in a map
+ * where the keys are the fully qualified class name, and the values are their associated
+ * {@link ViewElementDescriptor}.
+ */
+ private HashMap<IProject, HashMap<String, ViewElementDescriptor>> mCustomDescriptorMap =
+ new HashMap<IProject, HashMap<String, ViewElementDescriptor>>();
+
+ /**
+ * TODO will be used to update the ViewElementDescriptor of the custom view when it
+ * is modified (either the class itself or its attributes.xml)
+ */
+ @SuppressWarnings("unused")
+ private ICustomViewDescriptorListener mListener;
+
+ /**
+ * Classes which implements this interface provide a method that deal with modifications
+ * in custom View class triggering a change in its associated {@link ViewClassInfo} object.
+ */
+ public interface ICustomViewDescriptorListener {
+ /**
+ * Sent when a custom View class has changed and
+ * its {@link ViewElementDescriptor} was modified.
+ *
+ * @param project the project containing the class.
+ * @param className the fully qualified class name.
+ * @param descriptor the updated ElementDescriptor.
+ */
+ public void updatedClassInfo(IProject project,
+ String className,
+ ViewElementDescriptor descriptor);
+ }
+
+ /**
+ * Returns the singleton instance of {@link CustomViewDescriptorService}.
+ */
+ public static CustomViewDescriptorService getInstance() {
+ return sThis;
+ }
+
+ /**
+ * Sets the listener receiving custom View class modification notifications.
+ * @param listener the listener to receive the notifications.
+ *
+ * TODO will be used to update the ViewElementDescriptor of the custom view when it
+ * is modified (either the class itself or its attributes.xml)
+ */
+ public void setListener(ICustomViewDescriptorListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Returns the {@link ViewElementDescriptor} for a particular project/class when the
+ * fully qualified class name actually matches a class from the given project.
+ * <p/>
+ * Custom descriptors are created as needed.
+ * <p/>
+ * If it is the first time the {@link ViewElementDescriptor} is requested, the method
+ * will check that the specified class is in fact a custom View class. Once this is
+ * established, a monitoring for that particular class is initiated. Any change will
+ * trigger a notification to the {@link ICustomViewDescriptorListener}.
+ *
+ * @param project the project containing the class.
+ * @param fqcn the fully qualified name of the class.
+ * @return a {@link ViewElementDescriptor} or <code>null</code> if the class was not
+ * a custom View class.
+ */
+ public ViewElementDescriptor getDescriptor(IProject project, String fqcn) {
+ // look in the map first
+ synchronized (mCustomDescriptorMap) {
+ HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project);
+
+ if (map != null) {
+ ViewElementDescriptor descriptor = map.get(fqcn);
+ if (descriptor != null) {
+ return descriptor;
+ }
+ }
+
+ // if we step here, it looks like we haven't created it yet.
+ // First lets check this is in fact a valid type in the project
+
+ try {
+ // We expect the project to be both opened and of java type (since it's an android
+ // project), so we can create a IJavaProject object from our IProject.
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // replace $ by . in the class name
+ String javaClassName = fqcn.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // look for the IType object for this class
+ IType type = javaProject.findType(javaClassName);
+ if (type != null && type.exists()) {
+ // the type exists. Let's get the parent class and its ViewClassInfo.
+
+ // get the type hierarchy
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
+ new NullProgressMonitor());
+
+ ViewElementDescriptor parentDescriptor = createViewDescriptor(
+ hierarchy.getSuperclass(type), project, hierarchy);
+
+ if (parentDescriptor != null) {
+ // we have a valid parent, lets create a new ViewElementDescriptor.
+ List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>();
+ List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>();
+ Map<ResourceFile, Long> files = findCustomDescriptors(project, type,
+ attrList, paramList);
+
+ AttributeDescriptor[] attributes =
+ getAttributeDescriptor(type, parentDescriptor);
+ if (!attrList.isEmpty()) {
+ attributes = join(attrList, attributes);
+ }
+ AttributeDescriptor[] layoutAttributes =
+ getLayoutAttributeDescriptors(type, parentDescriptor);
+ if (!paramList.isEmpty()) {
+ layoutAttributes = join(paramList, layoutAttributes);
+ }
+ String name = DescriptorsUtils.getBasename(fqcn);
+ ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn,
+ attributes,
+ layoutAttributes,
+ parentDescriptor.getChildren(),
+ project, files);
+ descriptor.setSuperClass(parentDescriptor);
+
+ synchronized (mCustomDescriptorMap) {
+ map = mCustomDescriptorMap.get(project);
+ if (map == null) {
+ map = new HashMap<String, ViewElementDescriptor>();
+ mCustomDescriptorMap.put(project, map);
+ }
+
+ map.put(fqcn, descriptor);
+ }
+
+ //TODO setup listener on this resource change.
+
+ return descriptor;
+ }
+ }
+ } catch (JavaModelException e) {
+ // there was an error accessing any of the IType, we'll just return null;
+ }
+ }
+
+ return null;
+ }
+
+ private static AttributeDescriptor[] join(
+ @NonNull List<AttributeDescriptor> attributeList,
+ @NonNull AttributeDescriptor[] attributes) {
+ if (!attributeList.isEmpty()) {
+ return ObjectArrays.concat(
+ attributeList.toArray(new AttributeDescriptor[attributeList.size()]),
+ attributes,
+ AttributeDescriptor.class);
+ } else {
+ return attributes;
+ }
+
+ }
+
+ /** Cache used by {@link #getParser(ResourceFile)} */
+ private Map<ResourceFile, AttrsXmlParser> mParserCache;
+
+ private AttrsXmlParser getParser(ResourceFile file) {
+ if (mParserCache == null) {
+ mParserCache = new HashMap<ResourceFile, AttrsXmlParser>();
+ }
+
+ AttrsXmlParser parser = mParserCache.get(file);
+ if (parser == null) {
+ parser = new AttrsXmlParser(
+ file.getFile().getOsLocation(),
+ AdtPlugin.getDefault(), 20);
+ parser.preload();
+ mParserCache.put(file, parser);
+ }
+
+ return parser;
+ }
+
+ /** Compute/find the styleable resources for the given type, if possible */
+ private Map<ResourceFile, Long> findCustomDescriptors(
+ IProject project,
+ IType type,
+ List<AttributeDescriptor> customAttributes,
+ List<AttributeDescriptor> customLayoutAttributes) {
+ // Look up the project where the type is declared (could be a library project;
+ // we cannot use type.getJavaProject().getProject())
+ IProject library = getProjectDeclaringType(type);
+ if (library == null) {
+ library = project;
+ }
+
+ String className = type.getElementName();
+ Set<ResourceFile> resourceFiles = findAttrsFiles(library, className);
+ if (resourceFiles != null && resourceFiles.size() > 0) {
+ String appUri = getAppResUri(project);
+ Map<ResourceFile, Long> timestamps =
+ Maps.newHashMapWithExpectedSize(resourceFiles.size());
+ for (ResourceFile file : resourceFiles) {
+ AttrsXmlParser attrsXmlParser = getParser(file);
+ String fqcn = type.getFullyQualifiedName();
+
+ // Attributes
+ ViewClassInfo classInfo = new ViewClassInfo(true, fqcn, className);
+ attrsXmlParser.loadViewAttributes(classInfo);
+ appendAttributes(customAttributes, classInfo.getAttributes(), appUri);
+
+ // Layout params
+ LayoutParamsInfo layoutInfo = new ViewClassInfo.LayoutParamsInfo(
+ classInfo, "Layout", null /*superClassInfo*/); //$NON-NLS-1$
+ attrsXmlParser.loadLayoutParamsAttributes(layoutInfo);
+ appendAttributes(customLayoutAttributes, layoutInfo.getAttributes(), appUri);
+
+ timestamps.put(file, file.getFile().getModificationStamp());
+ }
+
+ return timestamps;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the set of XML files (if any) in the given library declaring
+ * attributes for the given class name
+ */
+ @Nullable
+ private static Set<ResourceFile> findAttrsFiles(IProject library, String className) {
+ Set<ResourceFile> resourceFiles = null;
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(library);
+ if (resources != null) {
+ Collection<ResourceItem> items =
+ resources.getResourceItemsOfType(ResourceType.DECLARE_STYLEABLE);
+ for (ResourceItem item : items) {
+ String viewName = item.getName();
+ if (viewName.equals(className)
+ || (viewName.startsWith(className)
+ && viewName.equals(className + "_Layout"))) { //$NON-NLS-1$
+ if (resourceFiles == null) {
+ resourceFiles = new HashSet<ResourceFile>();
+ }
+ resourceFiles.addAll(item.getSourceFileList());
+ }
+ }
+ }
+ return resourceFiles;
+ }
+
+ /**
+ * Find the project containing this type declaration. We cannot use
+ * {@link IType#getJavaProject()} since that will return the including
+ * project and we're after the library project such that we can find the
+ * attrs.xml file in the same project.
+ */
+ @Nullable
+ private static IProject getProjectDeclaringType(IType type) {
+ IClassFile classFile = type.getClassFile();
+ if (classFile != null) {
+ IPath path = classFile.getPath();
+ IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
+ IResource resource;
+ if (path.isAbsolute()) {
+ resource = AdtUtils.fileToResource(path.toFile());
+ } else {
+ resource = workspace.findMember(path);
+ }
+ if (resource != null && resource.getProject() != null) {
+ return resource.getProject();
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the name space to use for application attributes */
+ private static String getAppResUri(IProject project) {
+ String appResource;
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null && projectState.isLibrary()) {
+ appResource = AUTO_URI;
+ } else {
+ ManifestInfo manifestInfo = ManifestInfo.get(project);
+ appResource = URI_PREFIX + manifestInfo.getPackage();
+ }
+ return appResource;
+ }
+
+
+ /** Append the {@link AttributeInfo} objects converted {@link AttributeDescriptor}
+ * objects into the given attribute list.
+ * <p>
+ * This is nearly identical to
+ * {@link DescriptorsUtils#appendAttribute(List, String, String, AttributeInfo, boolean, Map)}
+ * but it handles namespace declarations in the attrs.xml file where the android:
+ * namespace is included in the names.
+ */
+ private static void appendAttributes(List<AttributeDescriptor> attributes,
+ AttributeInfo[] attributeInfos, String appResource) {
+ // Custom attributes
+ for (AttributeInfo info : attributeInfos) {
+ String nsUri;
+ if (info.getName().startsWith(ANDROID_NS_NAME_PREFIX)) {
+ info.setName(info.getName().substring(ANDROID_NS_NAME_PREFIX.length()));
+ nsUri = ANDROID_URI;
+ } else {
+ nsUri = appResource;
+ }
+
+ DescriptorsUtils.appendAttribute(attributes,
+ null /*elementXmlName*/, nsUri, info, false /*required*/,
+ null /*overrides*/);
+ }
+ }
+
+ /**
+ * Computes (if needed) and returns the {@link ViewElementDescriptor} for the specified type.
+ *
+ * @return A {@link ViewElementDescriptor} or null if type or typeHierarchy is null.
+ */
+ private ViewElementDescriptor createViewDescriptor(IType type, IProject project,
+ ITypeHierarchy typeHierarchy) {
+ // check if the type is a built-in View class.
+ List<ViewElementDescriptor> builtInList = null;
+
+ // give up if there's no type
+ if (type == null) {
+ return null;
+ }
+
+ String fqcn = type.getFullyQualifiedName();
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ LayoutDescriptors descriptors = data.getLayoutDescriptors();
+ ViewElementDescriptor d = descriptors.findDescriptorByClass(fqcn);
+ if (d != null) {
+ return d;
+ }
+ builtInList = descriptors.getViewDescriptors();
+ }
+ }
+ }
+
+ // it's not a built-in class? Lets look if the superclass is built-in
+ // give up if there's no type
+ if (typeHierarchy == null) {
+ return null;
+ }
+
+ IType parentType = typeHierarchy.getSuperclass(type);
+ if (parentType != null) {
+ ViewElementDescriptor parentDescriptor = createViewDescriptor(parentType, project,
+ typeHierarchy);
+
+ if (parentDescriptor != null) {
+ // parent class is a valid View class with a descriptor, so we create one
+ // for this class.
+ String name = DescriptorsUtils.getBasename(fqcn);
+ // A custom view accepts children if its parent descriptor also does.
+ // The only exception to this is ViewGroup, which accepts children even though
+ // its parent does not.
+ boolean isViewGroup = fqcn.equals(CLASS_VIEWGROUP);
+ boolean hasChildren = isViewGroup || parentDescriptor.hasChildren();
+ ViewElementDescriptor[] children = null;
+ if (hasChildren && builtInList != null) {
+ // We can't figure out what the allowable children are by just
+ // looking at the class, so assume any View is valid
+ children = builtInList.toArray(new ViewElementDescriptor[builtInList.size()]);
+ }
+ ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn,
+ getAttributeDescriptor(type, parentDescriptor),
+ getLayoutAttributeDescriptors(type, parentDescriptor),
+ children, project, null);
+ descriptor.setSuperClass(parentDescriptor);
+
+ // add it to the map
+ synchronized (mCustomDescriptorMap) {
+ HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project);
+
+ if (map == null) {
+ map = new HashMap<String, ViewElementDescriptor>();
+ mCustomDescriptorMap.put(project, map);
+ }
+
+ map.put(fqcn, descriptor);
+
+ }
+
+ //TODO setup listener on this resource change.
+
+ return descriptor;
+ }
+ }
+
+ // class is neither a built-in view class, nor extend one. return null.
+ return null;
+ }
+
+ /**
+ * Returns the array of {@link AttributeDescriptor} for the specified {@link IType}.
+ * <p/>
+ * The array should contain the descriptor for this type and all its supertypes.
+ *
+ * @param type the type for which the {@link AttributeDescriptor} are returned.
+ * @param parentDescriptor the {@link ViewElementDescriptor} of the direct superclass.
+ */
+ private static AttributeDescriptor[] getAttributeDescriptor(IType type,
+ ViewElementDescriptor parentDescriptor) {
+ // TODO add the class attribute descriptors to the parent descriptors.
+ return parentDescriptor.getAttributes();
+ }
+
+ private static AttributeDescriptor[] getLayoutAttributeDescriptors(IType type,
+ ViewElementDescriptor parentDescriptor) {
+ return parentDescriptor.getLayoutAttributes();
+ }
+
+ private class CustomViewDescriptor extends ViewElementDescriptor {
+ private Map<ResourceFile, Long> mTimeStamps;
+ private IProject mProject;
+
+ public CustomViewDescriptor(String name, String fqcn, AttributeDescriptor[] attributes,
+ AttributeDescriptor[] layoutAttributes,
+ ElementDescriptor[] children, IProject project,
+ Map<ResourceFile, Long> timestamps) {
+ super(
+ fqcn, // xml name
+ name, // ui name
+ fqcn, // full class name
+ fqcn, // tooltip
+ null, // sdk_url
+ attributes,
+ layoutAttributes,
+ children,
+ false // mandatory
+ );
+ mTimeStamps = timestamps;
+ mProject = project;
+ }
+
+ @Override
+ public Image getGenericIcon() {
+ IconFactory iconFactory = IconFactory.getInstance();
+
+ int index = mXmlName.lastIndexOf('.');
+ if (index != -1) {
+ return iconFactory.getIcon(mXmlName.substring(index + 1),
+ "customView"); //$NON-NLS-1$
+ }
+
+ return iconFactory.getIcon("customView"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean syncAttributes() {
+ // Check if any of the descriptors
+ if (mTimeStamps != null) {
+ // Prevent checking actual file timestamps too frequently on rapid burst calls
+ long now = System.currentTimeMillis();
+ if (now - sLastCheck < 1000) {
+ return true;
+ }
+ sLastCheck = now;
+
+ // Check whether the resource files (typically just one) which defined
+ // custom attributes for this custom view have changed, and if so,
+ // refresh the attribute descriptors.
+ // This doesn't work the cases where you add descriptors for a custom
+ // view after using it, or add attributes in a separate file, but those
+ // scenarios aren't quite as common (and would require a bit more expensive
+ // analysis.)
+ for (Map.Entry<ResourceFile, Long> entry : mTimeStamps.entrySet()) {
+ ResourceFile file = entry.getKey();
+ Long timestamp = entry.getValue();
+ boolean recompute = false;
+ if (file.getFile().getModificationStamp() > timestamp.longValue()) {
+ // One or more attributes changed: recompute
+ recompute = true;
+ mParserCache.remove(file);
+ }
+
+ if (recompute) {
+ IJavaProject javaProject = JavaCore.create(mProject);
+ String fqcn = getFullClassName();
+ IType type = null;
+ try {
+ type = javaProject.findType(fqcn);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ if (type == null || !type.exists()) {
+ return true;
+ }
+
+ List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>();
+ List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>();
+
+ mTimeStamps = findCustomDescriptors(mProject, type, attrList, paramList);
+
+ ViewElementDescriptor parentDescriptor = getSuperClassDesc();
+ AttributeDescriptor[] attributes =
+ getAttributeDescriptor(type, parentDescriptor);
+ if (!attrList.isEmpty()) {
+ attributes = join(attrList, attributes);
+ }
+ attributes = attrList.toArray(new AttributeDescriptor[attrList.size()]);
+ setAttributes(attributes);
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /** Timestamp of the most recent {@link CustomViewDescriptor#syncAttributes} check */
+ private static long sLastCheck;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java
new file mode 100644
index 000000000..7b2fe84f0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.descriptors;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TAG;
+import static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.REQUEST_FOCUS;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.SdkConstants.VIEW_MERGE;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.common.resources.platform.ViewClassInfo;
+import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.ClassAttributeDescriptor;
+import com.android.sdklib.IAndroidTarget;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+
+/**
+ * Complete description of the layout structure.
+ */
+public final class LayoutDescriptors implements IDescriptorProvider {
+ /** The document descriptor. Contains all layouts and views linked together. */
+ private DocumentDescriptor mRootDescriptor =
+ new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$
+
+ /** The list of all known ViewLayout descriptors. */
+ private List<ViewElementDescriptor> mLayoutDescriptors = Collections.emptyList();
+
+ /** Read-Only list of View Descriptors. */
+ private List<ViewElementDescriptor> mROLayoutDescriptors;
+
+ /** The list of all known View (not ViewLayout) descriptors. */
+ private List<ViewElementDescriptor> mViewDescriptors = Collections.emptyList();
+
+ /** Read-Only list of View Descriptors. */
+ private List<ViewElementDescriptor> mROViewDescriptors;
+
+ /** The descriptor matching android.view.View. */
+ private ViewElementDescriptor mBaseViewDescriptor;
+
+ /** Map from view full class name to view descriptor */
+ private Map<String, ViewElementDescriptor> mFqcnToDescriptor =
+ // As of 3.1 there are 58 items in this map
+ new HashMap<String, ViewElementDescriptor>(80);
+
+ /** Returns the document descriptor. Contains all layouts and views linked together. */
+ @Override
+ public DocumentDescriptor getDescriptor() {
+ return mRootDescriptor;
+ }
+
+ /** Returns the read-only list of all known ViewLayout descriptors. */
+ public List<ViewElementDescriptor> getLayoutDescriptors() {
+ return mROLayoutDescriptors;
+ }
+
+ /** Returns the read-only list of all known View (not ViewLayout) descriptors. */
+ public List<ViewElementDescriptor> getViewDescriptors() {
+ return mROViewDescriptors;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mRootDescriptor.getChildren();
+ }
+
+ /**
+ * Returns the descriptor matching android.view.View, which is guaranteed
+ * to be a {@link ViewElementDescriptor}.
+ */
+ public ViewElementDescriptor getBaseViewDescriptor() {
+ if (mBaseViewDescriptor == null) {
+ mBaseViewDescriptor = findDescriptorByClass(SdkConstants.CLASS_VIEW);
+ }
+ return mBaseViewDescriptor;
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then update them
+ * all at once.
+ * <p/>
+ * TODO: differentiate groups from views in the tree UI? => rely on icons
+ * <p/>
+ *
+ * @param views The list of views in the framework.
+ * @param layouts The list of layouts in the framework.
+ * @param styleMap A map from style names to style information provided by the SDK
+ * @param target The android target being initialized
+ */
+ public synchronized void updateDescriptors(ViewClassInfo[] views, ViewClassInfo[] layouts,
+ Map<String, DeclareStyleableInfo> styleMap, IAndroidTarget target) {
+
+ // This map links every ViewClassInfo to the ElementDescriptor we created.
+ // It is filled by convertView() and used later to fix the super-class hierarchy.
+ HashMap<ViewClassInfo, ViewElementDescriptor> infoDescMap =
+ new HashMap<ViewClassInfo, ViewElementDescriptor>();
+
+ ArrayList<ViewElementDescriptor> newViews = new ArrayList<ViewElementDescriptor>(40);
+ if (views != null) {
+ for (ViewClassInfo info : views) {
+ ViewElementDescriptor desc = convertView(info, infoDescMap);
+ newViews.add(desc);
+ mFqcnToDescriptor.put(desc.getFullClassName(), desc);
+ }
+ }
+
+ // Create <include> as a synthetic regular view.
+ // Note: ViewStub is already described by attrs.xml
+ insertInclude(newViews);
+
+ List<ViewElementDescriptor> newLayouts = new ArrayList<ViewElementDescriptor>(30);
+ if (layouts != null) {
+ for (ViewClassInfo info : layouts) {
+ ViewElementDescriptor desc = convertView(info, infoDescMap);
+ newLayouts.add(desc);
+ mFqcnToDescriptor.put(desc.getFullClassName(), desc);
+ }
+ }
+
+ // Find View and inherit all its layout attributes
+ AttributeDescriptor[] frameLayoutAttrs = findViewLayoutAttributes(
+ SdkConstants.CLASS_FRAMELAYOUT);
+
+ if (target.getVersion().getApiLevel() >= 4) {
+ ViewElementDescriptor fragmentTag = createFragment(frameLayoutAttrs, styleMap);
+ newViews.add(fragmentTag);
+ }
+
+ List<ElementDescriptor> newDescriptors = new ArrayList<ElementDescriptor>(80);
+ newDescriptors.addAll(newLayouts);
+ newDescriptors.addAll(newViews);
+
+ ViewElementDescriptor viewTag = createViewTag(frameLayoutAttrs);
+ newViews.add(viewTag);
+ newDescriptors.add(viewTag);
+
+ ViewElementDescriptor requestFocus = createRequestFocus();
+ newViews.add(requestFocus);
+ newDescriptors.add(requestFocus);
+
+ // Link all layouts to everything else here.. recursively
+ for (ViewElementDescriptor layoutDesc : newLayouts) {
+ layoutDesc.setChildren(newDescriptors);
+ }
+
+ // The gesture overlay descriptor is really a layout but not included in the layouts list
+ // so handle it specially
+ ViewElementDescriptor gestureView = findDescriptorByClass(FQCN_GESTURE_OVERLAY_VIEW);
+ if (gestureView != null) {
+ gestureView.setChildren(newDescriptors);
+ // Inherit layout attributes from FrameLayout
+ gestureView.setLayoutAttributes(frameLayoutAttrs);
+ }
+
+ fixSuperClasses(infoDescMap);
+
+ // The <merge> tag can only be a root tag, so it is added at the end.
+ // It gets everything else as children but it is not made a child itself.
+ ViewElementDescriptor mergeTag = createMerge(frameLayoutAttrs);
+ mergeTag.setChildren(newDescriptors); // mergeTag makes a copy of the list
+ newDescriptors.add(mergeTag);
+ newLayouts.add(mergeTag);
+
+ // Sort palette contents
+ Collections.sort(newViews);
+ Collections.sort(newLayouts);
+
+ mViewDescriptors = newViews;
+ mLayoutDescriptors = newLayouts;
+ mRootDescriptor.setChildren(newDescriptors);
+
+ mBaseViewDescriptor = null;
+ mROLayoutDescriptors = Collections.unmodifiableList(mLayoutDescriptors);
+ mROViewDescriptors = Collections.unmodifiableList(mViewDescriptors);
+ }
+
+ /**
+ * Creates an element descriptor from a given {@link ViewClassInfo}.
+ *
+ * @param info The {@link ViewClassInfo} to convert into a new {@link ViewElementDescriptor}.
+ * @param infoDescMap This map links every ViewClassInfo to the ElementDescriptor it created.
+ * It is filled by here and used later to fix the super-class hierarchy.
+ */
+ private ViewElementDescriptor convertView(
+ ViewClassInfo info,
+ HashMap<ViewClassInfo, ViewElementDescriptor> infoDescMap) {
+ String xmlName = info.getShortClassName();
+ String uiName = xmlName;
+ String fqcn = info.getFullClassName();
+ if (ViewElementDescriptor.viewNeedsPackage(fqcn)) {
+ xmlName = fqcn;
+ }
+ String tooltip = info.getJavaDoc();
+
+ // Average is around 90, max (in 3.2) is 145
+ ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>(120);
+
+ // All views and groups have an implicit "style" attribute which is a reference.
+ AttributeInfo styleInfo = new AttributeInfo(
+ "style", //$NON-NLS-1$ xmlLocalName
+ Format.REFERENCE_SET);
+ styleInfo.setJavaDoc("A reference to a custom style"); //tooltip
+ DescriptorsUtils.appendAttribute(attributes,
+ "style", //$NON-NLS-1$
+ null, //nsUri
+ styleInfo,
+ false, //required
+ null); // overrides
+ styleInfo.setDefinedBy(SdkConstants.CLASS_VIEW);
+
+ // Process all View attributes
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ ANDROID_URI,
+ info.getAttributes(),
+ null, // requiredAttributes
+ null /* overrides */);
+
+ List<String> attributeSources = new ArrayList<String>();
+ if (info.getAttributes() != null && info.getAttributes().length > 0) {
+ attributeSources.add(fqcn);
+ }
+
+ for (ViewClassInfo link = info.getSuperClass();
+ link != null;
+ link = link.getSuperClass()) {
+ AttributeInfo[] attrList = link.getAttributes();
+ if (attrList.length > 0) {
+ attributeSources.add(link.getFullClassName());
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ ANDROID_URI,
+ attrList,
+ null, // requiredAttributes
+ null /* overrides */);
+ }
+ }
+
+ // Process all LayoutParams attributes
+ ArrayList<AttributeDescriptor> layoutAttributes = new ArrayList<AttributeDescriptor>();
+ LayoutParamsInfo layoutParams = info.getLayoutData();
+
+ for(; layoutParams != null; layoutParams = layoutParams.getSuperClass()) {
+ for (AttributeInfo attrInfo : layoutParams.getAttributes()) {
+ if (DescriptorsUtils.containsAttribute(layoutAttributes,
+ ANDROID_URI, attrInfo)) {
+ continue;
+ }
+ DescriptorsUtils.appendAttribute(layoutAttributes,
+ null, // elementName
+ ANDROID_URI,
+ attrInfo,
+ false, // required
+ null /* overrides */);
+ }
+ }
+
+ ViewElementDescriptor desc = new ViewElementDescriptor(
+ xmlName,
+ uiName,
+ fqcn,
+ tooltip,
+ null, // sdk_url
+ attributes.toArray(new AttributeDescriptor[attributes.size()]),
+ layoutAttributes.toArray(new AttributeDescriptor[layoutAttributes.size()]),
+ null, // children
+ false /* mandatory */);
+ desc.setAttributeSources(Collections.unmodifiableList(attributeSources));
+ infoDescMap.put(info, desc);
+ return desc;
+ }
+
+ /**
+ * Creates a new {@code <include>} descriptor and adds it to the list of view descriptors.
+ *
+ * @param knownViews A list of view descriptors being populated. Also used to find the
+ * View descriptor and extract its layout attributes.
+ */
+ private void insertInclude(List<ViewElementDescriptor> knownViews) {
+ String xmlName = VIEW_INCLUDE;
+
+ // Create the include custom attributes
+ ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
+
+ // Find View and inherit all its layout attributes
+ AttributeDescriptor[] viewLayoutAttribs;
+ AttributeDescriptor[] viewAttributes = null;
+ ViewElementDescriptor viewDesc = findDescriptorByClass(SdkConstants.CLASS_VIEW);
+ if (viewDesc != null) {
+ viewAttributes = viewDesc.getAttributes();
+ attributes = new ArrayList<AttributeDescriptor>(viewAttributes.length + 1);
+ viewLayoutAttribs = viewDesc.getLayoutAttributes();
+ } else {
+ viewLayoutAttribs = new AttributeDescriptor[0];
+ }
+
+ // Note that the "layout" attribute does NOT have the Android namespace
+ DescriptorsUtils.appendAttribute(attributes,
+ null, //elementXmlName
+ null, //nsUri
+ new AttributeInfo(
+ ATTR_LAYOUT,
+ Format.REFERENCE_SET ),
+ true, //required
+ null); //overrides
+
+ if (viewAttributes != null) {
+ for (AttributeDescriptor descriptor : viewAttributes) {
+ attributes.add(descriptor);
+ }
+ }
+
+ // Create the include descriptor
+ ViewElementDescriptor desc = new ViewElementDescriptor(xmlName,
+ xmlName, // ui_name
+ VIEW_INCLUDE, // "class name"; the GLE only treats this as an element tag
+ "Lets you statically include XML layouts inside other XML layouts.", // tooltip
+ null, // sdk_url
+ attributes.toArray(new AttributeDescriptor[attributes.size()]),
+ viewLayoutAttribs, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ knownViews.add(desc);
+ }
+
+ /**
+ * Creates and returns a new {@code <merge>} descriptor.
+ * @param viewLayoutAttribs The layout attributes to use for the new descriptor
+ */
+ private ViewElementDescriptor createMerge(AttributeDescriptor[] viewLayoutAttribs) {
+ String xmlName = VIEW_MERGE;
+
+ // Create the include descriptor
+ ViewElementDescriptor desc = new ViewElementDescriptor(xmlName,
+ xmlName, // ui_name
+ VIEW_MERGE, // "class name"; the GLE only treats this as an element tag
+ "A root tag useful for XML layouts inflated using a ViewStub.", // tooltip
+ null, // sdk_url
+ null, // attributes
+ viewLayoutAttribs, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ return desc;
+ }
+
+ /**
+ * Creates and returns a new {@code <fragment>} descriptor.
+ * @param viewLayoutAttribs The layout attributes to use for the new descriptor
+ * @param styleMap The style map provided by the SDK
+ */
+ private ViewElementDescriptor createFragment(AttributeDescriptor[] viewLayoutAttribs,
+ Map<String, DeclareStyleableInfo> styleMap) {
+ String xmlName = VIEW_FRAGMENT;
+ final ViewElementDescriptor descriptor;
+
+ // First try to create the descriptor from metadata in attrs.xml:
+ DeclareStyleableInfo style = styleMap.get("Fragment"); //$NON-NLS-1$
+ String fragmentTooltip =
+ "A Fragment is a piece of an application's user interface or behavior that "
+ + "can be placed in an Activity";
+ String sdkUrl = "http://developer.android.com/guide/topics/fundamentals/fragments.html";
+ TextAttributeDescriptor classAttribute = new ClassAttributeDescriptor(
+ // Should accept both CLASS_V4_FRAGMENT and CLASS_FRAGMENT
+ null /*superClassName*/,
+ ATTR_CLASS, null /* namespace */,
+ new AttributeInfo(ATTR_CLASS, Format.STRING_SET),
+ true /*mandatory*/)
+ .setTooltip("Supply the name of the fragment class to instantiate");
+
+ if (style != null) {
+ descriptor = new ViewElementDescriptor(
+ VIEW_FRAGMENT, VIEW_FRAGMENT, VIEW_FRAGMENT,
+ fragmentTooltip, // tooltip
+ sdkUrl, //,
+ null /* attributes */,
+ viewLayoutAttribs, // layout attributes
+ null /*childrenElements*/,
+ false /*mandatory*/);
+ ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>();
+ // The class attribute is not included in the attrs.xml
+ descs.add(classAttribute);
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ ANDROID_URI,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ //descriptor.setTooltip(style.getJavaDoc());
+ descriptor.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()]));
+ } else {
+ // The above will only work on API 11 and up. However, fragments are *also* available
+ // on older platforms, via the fragment support library, so add in a manual
+ // entry if necessary.
+ descriptor = new ViewElementDescriptor(xmlName,
+ xmlName, // ui_name
+ xmlName, // "class name"; the GLE only treats this as an element tag
+ fragmentTooltip,
+ sdkUrl,
+ new AttributeDescriptor[] {
+ new ClassAttributeDescriptor(
+ null /*superClassName*/,
+ ATTR_NAME, ANDROID_URI,
+ new AttributeInfo(ATTR_NAME, Format.STRING_SET),
+ true /*mandatory*/)
+ .setTooltip("Supply the name of the fragment class to instantiate"),
+ classAttribute,
+ new ClassAttributeDescriptor(
+ null /*superClassName*/,
+ ATTR_TAG, ANDROID_URI,
+ new AttributeInfo(ATTR_TAG, Format.STRING_SET),
+ true /*mandatory*/)
+ .setTooltip("Supply a tag for the top-level view containing a String"),
+ }, // attributes
+ viewLayoutAttribs, // layout attributes
+ null, // children
+ false /* mandatory */);
+ }
+
+ return descriptor;
+ }
+
+ /**
+ * Creates and returns a new {@code <view>} descriptor.
+ * @param viewLayoutAttribs The layout attributes to use for the new descriptor
+ * @param styleMap The style map provided by the SDK
+ */
+ private ViewElementDescriptor createViewTag(AttributeDescriptor[] viewLayoutAttribs) {
+ String xmlName = VIEW_TAG;
+
+ TextAttributeDescriptor classAttribute = new ClassAttributeDescriptor(
+ CLASS_VIEW,
+ ATTR_CLASS, null /* namespace */,
+ new AttributeInfo(ATTR_CLASS, Format.STRING_SET),
+ true /*mandatory*/)
+ .setTooltip("Supply the name of the view class to instantiate");
+
+ // Create the include descriptor
+ ViewElementDescriptor desc = new ViewElementDescriptor(xmlName,
+ xmlName, // ui_name
+ xmlName, // "class name"; the GLE only treats this as an element tag
+ "A view tag whose class attribute names the class to be instantiated", // tooltip
+ null, // sdk_url
+ new AttributeDescriptor[] { // attributes
+ classAttribute
+ },
+ viewLayoutAttribs, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ return desc;
+ }
+
+ /**
+ * Creates and returns a new {@code <requestFocus>} descriptor.
+ */
+ private ViewElementDescriptor createRequestFocus() {
+ String xmlName = REQUEST_FOCUS;
+
+ // Create the include descriptor
+ return new ViewElementDescriptor(
+ xmlName, // xml_name
+ xmlName, // ui_name
+ xmlName, // "class name"; the GLE only treats this as an element tag
+ "Requests focus for the parent element or one of its descendants", // tooltip
+ null, // sdk_url
+ null, // attributes
+ null, // layout attributes
+ null, // children
+ false /* mandatory */);
+ }
+
+ /**
+ * Finds the descriptor and retrieves all its layout attributes.
+ */
+ private AttributeDescriptor[] findViewLayoutAttributes(
+ String viewFqcn) {
+ ViewElementDescriptor viewDesc = findDescriptorByClass(viewFqcn);
+ if (viewDesc != null) {
+ return viewDesc.getLayoutAttributes();
+ }
+
+ return null;
+ }
+
+ /**
+ * Set the super-class of each {@link ViewElementDescriptor} by using the super-class
+ * information available in the {@link ViewClassInfo}.
+ */
+ private void fixSuperClasses(Map<ViewClassInfo, ViewElementDescriptor> infoDescMap) {
+
+ for (Entry<ViewClassInfo, ViewElementDescriptor> entry : infoDescMap.entrySet()) {
+ ViewClassInfo info = entry.getKey();
+ ViewElementDescriptor desc = entry.getValue();
+
+ ViewClassInfo sup = info.getSuperClass();
+ if (sup != null) {
+ ViewElementDescriptor supDesc = infoDescMap.get(sup);
+ while (supDesc == null && sup != null) {
+ // We don't have a descriptor for the super-class. That means the class is
+ // probably abstract, so we just need to walk up the super-class chain till
+ // we find one we have. All views derive from android.view.View so we should
+ // surely find that eventually.
+ sup = sup.getSuperClass();
+ if (sup != null) {
+ supDesc = infoDescMap.get(sup);
+ }
+ }
+ if (supDesc != null) {
+ desc.setSuperClass(supDesc);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link ViewElementDescriptor} with the given fully qualified class
+ * name, or null if not found. This is a quick map lookup.
+ *
+ * @param fqcn the fully qualified class name
+ * @return the corresponding {@link ViewElementDescriptor} or null
+ */
+ public ViewElementDescriptor findDescriptorByClass(String fqcn) {
+ return mFqcnToDescriptor.get(fqcn);
+ }
+
+ /**
+ * Returns the {@link ViewElementDescriptor} with the given XML tag name,
+ * which usually does not include the package (depending on the
+ * value of {@link ViewElementDescriptor#viewNeedsPackage(String)}).
+ *
+ * @param tag the XML tag name
+ * @return the corresponding {@link ViewElementDescriptor} or null
+ */
+ public ViewElementDescriptor findDescriptorByTag(String tag) {
+ // TODO: Consider whether we need to add a direct map lookup for this as well.
+ // Currently not done since this is not frequently needed (only needed for
+ // exploded rendering which was already performing list iteration.)
+ for (ViewElementDescriptor descriptor : mLayoutDescriptors) {
+ if (tag.equals(descriptor.getXmlLocalName())) {
+ return descriptor;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a collection of all the view class names, including layouts
+ *
+ * @return a collection of all the view class names, never null
+ */
+ public Collection<String> getAllViewClassNames() {
+ return mFqcnToDescriptor.keySet();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java
new file mode 100644
index 000000000..79995249c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.descriptors;
+
+import static com.android.SdkConstants.ANDROID_VIEW_PKG;
+import static com.android.SdkConstants.ANDROID_WEBKIT_PKG;
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.VIEW;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link ViewElementDescriptor} describes the properties expected for a given XML element node
+ * representing a class in an XML Layout file.
+ * <p/>
+ * These descriptors describe Android views XML elements.
+ * <p/>
+ * The base class {@link ElementDescriptor} has a notion of "children", that is an XML element
+ * can produce another set of XML elements. Because of the flat nature of Android's layout
+ * XML files all possible views are children of the document and of themselves (that is any
+ * view group can contain any other view). This is an implied contract of this class that is
+ * enforces at construction by {@link LayoutDescriptors}. Note that by construction any code
+ * that deals with the children hierarchy must also deal with potential infinite loops since views
+ * <em>will</em> reference themselves (e.g. a ViewGroup can contain a ViewGroup).
+ * <p/>
+ * Since Views are also Java classes, they derive from each other. Here this is represented
+ * as the "super class", which denotes the fact that a given View java class derives from
+ * another class. These properties are also set at construction by {@link LayoutDescriptors}.
+ * The super class hierarchy is very different from the descriptor's children hierarchy: the
+ * later represents Java inheritance, the former represents an XML nesting capability.
+ *
+ * @see ElementDescriptor
+ */
+public class ViewElementDescriptor extends ElementDescriptor {
+
+ /** The full class name (FQCN) of this view. */
+ private final String mFullClassName;
+
+ /** The list of layout attributes. Can be empty but not null. */
+ private AttributeDescriptor[] mLayoutAttributes;
+
+ /** The super-class descriptor. Can be null. */
+ private ViewElementDescriptor mSuperClassDesc;
+
+ /** List of attribute sources, classes that contribute attributes to {@link #mAttributes} */
+ private List<String> mAttributeSources;
+
+ /**
+ * Constructs a new {@link ViewElementDescriptor} based on its XML name, UI name,
+ * the canonical name of the class it represents, its tooltip, its SDK url, its attributes list,
+ * its children list and its mandatory flag.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param fullClassName The fully qualified class name the {@link ViewElementDescriptor} is
+ * representing.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param layoutAttributes The list of layout attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ViewElementDescriptor(String xml_name, String ui_name,
+ String fullClassName,
+ String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes, AttributeDescriptor[] layoutAttributes,
+ ElementDescriptor[] children, boolean mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ mFullClassName = fullClassName;
+ mLayoutAttributes = layoutAttributes != null ? layoutAttributes : new AttributeDescriptor[0];
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and on the canonical
+ * name of the class it represents.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param fullClassName The fully qualified class name the {@link ViewElementDescriptor} is
+ * representing.
+ */
+ public ViewElementDescriptor(String xml_name, String fullClassName) {
+ super(xml_name);
+ mFullClassName = fullClassName;
+ mLayoutAttributes = null;
+ }
+
+ /**
+ * Returns the fully qualified name of the View class represented by this element descriptor
+ * e.g. "android.view.View".
+ *
+ * @return the fully qualified class name, never null
+ */
+ public String getFullClassName() {
+ return mFullClassName;
+ }
+
+ /** Returns the list of layout attributes. Can be empty but not null.
+ *
+ * @return the list of layout attributes, never null
+ */
+ public AttributeDescriptor[] getLayoutAttributes() {
+ return mLayoutAttributes;
+ }
+
+ /**
+ * Sets the list of layout attribute attributes.
+ *
+ * @param attributes the new layout attributes, not null
+ */
+ public void setLayoutAttributes(AttributeDescriptor[] attributes) {
+ assert attributes != null;
+ mLayoutAttributes = attributes;
+ }
+
+ /**
+ * Returns a new {@link UiViewElementNode} linked to this descriptor.
+ */
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiViewElementNode(this);
+ }
+
+ /**
+ * Returns the {@link ViewElementDescriptor} of the super-class of this View descriptor
+ * that matches the java View hierarchy. Can be null.
+ *
+ * @return the super class' descriptor or null
+ */
+ public ViewElementDescriptor getSuperClassDesc() {
+ return mSuperClassDesc;
+ }
+
+ /**
+ * Sets the {@link ViewElementDescriptor} of the super-class of this View descriptor
+ * that matches the java View hierarchy. Can be null.
+ *
+ * @param superClassDesc the descriptor for the super class, or null
+ */
+ public void setSuperClass(ViewElementDescriptor superClassDesc) {
+ mSuperClassDesc = superClassDesc;
+ }
+
+ /**
+ * Returns an optional icon for the element.
+ * <p/>
+ * By default this tries to return an icon based on the XML name of the element.
+ * If this fails, it tries to return the default element icon as defined in the
+ * plugin. If all fails, it returns null.
+ *
+ * @return An icon for this element or null.
+ */
+ @Override
+ public Image getGenericIcon() {
+ IconFactory factory = IconFactory.getInstance();
+ String name = mXmlName;
+ if (name.indexOf('.') != -1) {
+ // If the user uses a fully qualified name, such as
+ // "android.gesture.GestureOverlayView" in their XML, we need to look up
+ // only by basename
+ name = name.substring(name.lastIndexOf('.') + 1);
+ } else if (VIEW_TAG.equals(name)) {
+ // Can't have both view.png and View.png; issues on case sensitive vs
+ // case insensitive file systems
+ name = VIEW;
+ }
+
+ Image icon = factory.getIcon(name);
+ if (icon == null) {
+ icon = AdtPlugin.getAndroidLogo();
+ }
+
+ return icon;
+ }
+
+ /**
+ * Returns the list of attribute sources for the attributes provided by this
+ * descriptor. An attribute source is the fully qualified class name of the
+ * defining class for some of the properties. The specific attribute source
+ * of a given {@link AttributeInfo} can be found by calling
+ * {@link AttributeInfo#getDefinedBy()}.
+ * <p>
+ * The attribute sources are ordered from class to super class.
+ * <p>
+ * The list may <b>not</b> be modified by clients.
+ *
+ * @return a non null list of attribute sources for this view
+ */
+ public List<String> getAttributeSources() {
+ return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList();
+ }
+
+ /**
+ * Sets the attribute sources for this view. See {@link #getAttributes()}
+ * for details.
+ *
+ * @param attributeSources a non null list of attribute sources for this
+ * view descriptor
+ * @see #getAttributeSources()
+ */
+ public void setAttributeSources(List<String> attributeSources) {
+ mAttributeSources = attributeSources;
+ }
+
+ /**
+ * Returns true if views with the given fully qualified class name need to include
+ * their package in the layout XML tag
+ *
+ * @param fqcn the fully qualified class name, such as android.widget.Button
+ * @return true if the full package path should be included in the layout XML element
+ * tag
+ */
+ public static boolean viewNeedsPackage(String fqcn) {
+ return !(fqcn.startsWith(ANDROID_WIDGET_PREFIX)
+ || fqcn.startsWith(ANDROID_VIEW_PKG)
+ || fqcn.startsWith(ANDROID_WEBKIT_PKG));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java
new file mode 100644
index 000000000..b3dce0756
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.ScrollBar;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The accordion control allows a series of labels with associated content that can be
+ * shown. For more details on accordions, see http://en.wikipedia.org/wiki/Accordion_(GUI)
+ * <p>
+ * This control allows the children to be created lazily. You can also customize the
+ * composite which is created to hold the children items, to for example allow multiple
+ * columns of items rather than just the default vertical stack.
+ * <p>
+ * The visual appearance of the headers is built in; it uses a mild gradient, with a
+ * heavier gradient during mouse-overs. It also uses a bold label along with the eclipse
+ * folder icons.
+ * <p>
+ * The control can be configured to enforce a single category open at any time (the
+ * default), or allowing multiple categories open (where they share the available space).
+ * The control can also be configured to fill the available vertical space for the open
+ * category/categories.
+ */
+public abstract class AccordionControl extends Composite {
+ /** Pixel spacing between header items */
+ private static final int HEADER_SPACING = 0;
+
+ /** Pixel spacing between items in the content area */
+ private static final int ITEM_SPACING = 0;
+
+ private static final String KEY_CONTENT = "content"; //$NON-NLS-1$
+ private static final String KEY_HEADER = "header"; //$NON-NLS-1$
+
+ private Image mClosed;
+ private Image mOpen;
+ private boolean mSingle = true;
+ private boolean mWrap;
+
+ /**
+ * Creates the container which will hold the items in a category; this can be
+ * overridden to lay out the children with a different layout than the default
+ * vertical RowLayout
+ */
+ protected Composite createChildContainer(Composite parent, Object header, int style) {
+ Composite composite = new Composite(parent, style);
+ if (mWrap) {
+ RowLayout layout = new RowLayout(SWT.HORIZONTAL);
+ layout.center = true;
+ composite.setLayout(layout);
+ } else {
+ RowLayout layout = new RowLayout(SWT.VERTICAL);
+ layout.spacing = ITEM_SPACING;
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ layout.marginLeft = 0;
+ layout.marginTop = 0;
+ layout.marginRight = 0;
+ layout.marginBottom = 0;
+ composite.setLayout(layout);
+ }
+
+ // TODO - maybe do multi-column arrangement for simple nodes
+ return composite;
+ }
+
+ /**
+ * Creates the children under a particular header
+ *
+ * @param parent the parent composite to add the SWT items to
+ * @param header the header object that is being opened for the first time
+ */
+ protected abstract void createChildren(Composite parent, Object header);
+
+ /**
+ * Set whether a single category should be enforced or not (default=true)
+ *
+ * @param single if true, enforce a single category open at a time
+ */
+ public void setAutoClose(boolean single) {
+ mSingle = single;
+ }
+
+ /**
+ * Returns whether a single category should be enforced or not (default=true)
+ *
+ * @return true if only a single category can be open at a time
+ */
+ public boolean isAutoClose() {
+ return mSingle;
+ }
+
+ /**
+ * Returns the labels used as header categories
+ *
+ * @return list of header labels
+ */
+ public List<CLabel> getHeaderLabels() {
+ List<CLabel> headers = new ArrayList<CLabel>();
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ headers.add((CLabel) c);
+ }
+ }
+
+ return headers;
+ }
+
+ /**
+ * Show all categories
+ *
+ * @param performLayout if true, call {@link #layout} and {@link #pack} when done
+ */
+ public void expandAll(boolean performLayout) {
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ if (!isOpen(c)) {
+ toggle((CLabel) c, false, false);
+ }
+ }
+ }
+ if (performLayout) {
+ pack();
+ layout();
+ }
+ }
+
+ /**
+ * Hide all categories
+ *
+ * @param performLayout if true, call {@link #layout} and {@link #pack} when done
+ */
+ public void collapseAll(boolean performLayout) {
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ if (isOpen(c)) {
+ toggle((CLabel) c, false, false);
+ }
+ }
+ }
+ if (performLayout) {
+ layout();
+ }
+ }
+
+ /**
+ * Create the composite.
+ *
+ * @param parent the parent widget to add the accordion to
+ * @param style the SWT style mask to use
+ * @param headers a list of headers, whose {@link Object#toString} method should
+ * produce the heading label
+ * @param greedy if true, grow vertically as much as possible
+ * @param wrapChildren if true, configure the child area to be horizontally laid out
+ * with wrapping
+ * @param expand Set of headers to expand initially
+ */
+ public AccordionControl(Composite parent, int style, List<?> headers,
+ boolean greedy, boolean wrapChildren, Set<String> expand) {
+ super(parent, style);
+ mWrap = wrapChildren;
+
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.verticalSpacing = HEADER_SPACING;
+ gridLayout.horizontalSpacing = 0;
+ gridLayout.marginWidth = 0;
+ gridLayout.marginHeight = 0;
+ setLayout(gridLayout);
+
+ Font labelFont = null;
+
+ mOpen = IconFactory.getInstance().getIcon("open-folder"); //$NON-NLS-1$
+ mClosed = IconFactory.getInstance().getIcon("closed-folder"); //$NON-NLS-1$
+ List<CLabel> expandLabels = new ArrayList<CLabel>();
+
+ for (Object header : headers) {
+ final CLabel label = new CLabel(this, SWT.SHADOW_OUT);
+ label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
+ updateBackground(label, false);
+ if (labelFont == null) {
+ labelFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT);
+ }
+ label.setFont(labelFont);
+ label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ setHeader(header, label);
+ label.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK) == 0) {
+ toggle(label, true, mSingle);
+ }
+ }
+ });
+ label.addMouseTrackListener(new MouseTrackListener() {
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ updateBackground(label, true);
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ updateBackground(label, false);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+ });
+
+ // Turn off border?
+ final ScrolledComposite scrolledComposite = new ScrolledComposite(this, SWT.V_SCROLL);
+ ScrollBar verticalBar = scrolledComposite.getVerticalBar();
+ verticalBar.setIncrement(20);
+ verticalBar.setPageIncrement(100);
+
+ // Do we need the scrolled composite or can we just look at the next
+ // wizard in the hierarchy?
+
+ setContentArea(label, scrolledComposite);
+ scrolledComposite.setExpandHorizontal(true);
+ scrolledComposite.setExpandVertical(true);
+ GridData scrollGridData = new GridData(SWT.FILL,
+ greedy ? SWT.FILL : SWT.TOP, false, greedy, 1, 1);
+ scrollGridData.exclude = true;
+ scrollGridData.grabExcessHorizontalSpace = wrapChildren;
+ scrolledComposite.setLayoutData(scrollGridData);
+
+ if (wrapChildren) {
+ scrolledComposite.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = scrolledComposite.getClientArea();
+ Control content = scrolledComposite.getContent();
+ if (content != null && r != null) {
+ Point minSize = content.computeSize(r.width, SWT.DEFAULT);
+ scrolledComposite.setMinSize(minSize);
+ ScrollBar vBar = scrolledComposite.getVerticalBar();
+ vBar.setPageIncrement(r.height);
+ }
+ }
+ });
+ }
+
+ updateIcon(label);
+ if (expand != null && expand.contains(label.getText())) {
+ // Comparing "label.getText()" rather than "header" because we make some
+ // tweaks to the label (replacing & with && etc) and in the getExpandedCategories
+ // method we return the label texts
+ expandLabels.add(label);
+ }
+ }
+
+ // Expand the requested categories
+ for (CLabel label : expandLabels) {
+ toggle(label, false, false);
+ }
+ }
+
+ /** Updates the background gradient of the given header label */
+ private void updateBackground(CLabel label, boolean mouseOver) {
+ Display display = label.getDisplay();
+ label.setBackground(new Color[] {
+ display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW),
+ display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND),
+ display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)
+ }, new int[] {
+ mouseOver ? 60 : 40, 100
+ }, true);
+ }
+
+ /**
+ * Updates the icon for a header label to be open/close based on the {@link #isOpen}
+ * state
+ */
+ private void updateIcon(CLabel label) {
+ label.setImage(isOpen(label) ? mOpen : mClosed);
+ }
+
+ /** Returns true if the content area for the given label is open/showing */
+ private boolean isOpen(Control label) {
+ return !((GridData) getContentArea(label).getLayoutData()).exclude;
+ }
+
+ /** Toggles the visibility of the children of the given label */
+ private void toggle(CLabel label, boolean performLayout, boolean autoClose) {
+ if (autoClose) {
+ collapseAll(true);
+ }
+ ScrolledComposite scrolledComposite = getContentArea(label);
+
+ GridData scrollGridData = (GridData) scrolledComposite.getLayoutData();
+ boolean close = !scrollGridData.exclude;
+ scrollGridData.exclude = close;
+ scrolledComposite.setVisible(!close);
+ updateIcon(label);
+
+ if (!scrollGridData.exclude && scrolledComposite.getContent() == null) {
+ Object header = getHeader(label);
+ Composite composite = createChildContainer(scrolledComposite, header, SWT.NONE);
+ createChildren(composite, header);
+ while (composite.getParent() != scrolledComposite) {
+ composite = composite.getParent();
+ }
+ scrolledComposite.setContent(composite);
+ scrolledComposite.setMinSize(composite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+ }
+
+ if (performLayout) {
+ layout(true);
+ }
+ }
+
+ /** Returns the header object for the given header label */
+ private Object getHeader(Control label) {
+ return label.getData(KEY_HEADER);
+ }
+
+ /** Sets the header object for the given header label */
+ private void setHeader(Object header, final CLabel label) {
+ label.setData(KEY_HEADER, header);
+ }
+
+ /** Returns the content area for the given header label */
+ private ScrolledComposite getContentArea(Control label) {
+ return (ScrolledComposite) label.getData(KEY_CONTENT);
+ }
+
+ /** Sets the content area for the given header label */
+ private void setContentArea(final CLabel label, ScrolledComposite scrolledComposite) {
+ label.setData(KEY_CONTENT, scrolledComposite);
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+
+ /**
+ * Returns the set of expanded categories in the palette. Note: Header labels will have
+ * escaped ampersand characters with double ampersands.
+ *
+ * @return the set of expanded categories in the palette - never null
+ */
+ public Set<String> getExpandedCategories() {
+ Set<String> expanded = new HashSet<String>();
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ if (isOpen(c)) {
+ expanded.add(((CLabel) c).getText());
+ }
+ }
+ }
+
+ return expanded;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java
new file mode 100644
index 000000000..9fc2e0937
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+/**
+ * This class implements 2D bin packing: packing rectangles into a given area as
+ * tightly as "possible" (bin packing in general is NP hard, so this class uses
+ * heuristics).
+ * <p>
+ * The algorithm implemented is to keep a set of (possibly overlapping)
+ * available areas for placement. For each newly inserted rectangle, we first
+ * pick which available space to occupy, and we then subdivide the
+ * current rectangle into all the possible remaining unoccupied sub-rectangles.
+ * We also remove any other space rectangles which are no longer eligible if
+ * they are intersecting the newly placed rectangle.
+ * <p>
+ * This algorithm is not very fast, so should not be used for a large number of
+ * rectangles.
+ */
+class BinPacker {
+ /**
+ * When enabled, the successive passes are dumped as PNG images showing the
+ * various available and occupied rectangles)
+ */
+ private static final boolean DEBUG = false;
+
+ private final List<Rect> mSpace = new ArrayList<Rect>();
+ private final int mMinHeight;
+ private final int mMinWidth;
+
+ /**
+ * Creates a new {@linkplain BinPacker}. To use it, first add one or more
+ * initial available space rectangles with {@link #addSpace(Rect)}, and then
+ * place the rectangles with {@link #occupy(int, int)}. The returned
+ * {@link Rect} from {@link #occupy(int, int)} gives the coordinates of the
+ * positioned rectangle.
+ *
+ * @param minWidth the smallest width of any rectangle placed into this bin
+ * @param minHeight the smallest height of any rectangle placed into this bin
+ */
+ BinPacker(int minWidth, int minHeight) {
+ mMinWidth = minWidth;
+ mMinHeight = minHeight;
+
+ if (DEBUG) {
+ mAllocated = new ArrayList<Rect>();
+ sLayoutId++;
+ sRectId = 1;
+ }
+ }
+
+ /** Adds more available space */
+ void addSpace(Rect rect) {
+ if (rect.w >= mMinWidth && rect.h >= mMinHeight) {
+ mSpace.add(rect);
+ }
+ }
+
+ /** Attempts to place a rectangle of the given dimensions, if possible */
+ @Nullable
+ Rect occupy(int width, int height) {
+ int index = findBest(width, height);
+ if (index == -1) {
+ return null;
+ }
+
+ return split(index, width, height);
+ }
+
+ /**
+ * Finds the best available space rectangle to position a new rectangle of
+ * the given size in.
+ */
+ private int findBest(int width, int height) {
+ if (mSpace.isEmpty()) {
+ return -1;
+ }
+
+ // Try to pack as far up as possible first
+ int bestIndex = -1;
+ boolean multipleAtSameY = false;
+ int minY = Integer.MAX_VALUE;
+ for (int i = 0, n = mSpace.size(); i < n; i++) {
+ Rect rect = mSpace.get(i);
+ if (rect.y <= minY) {
+ if (rect.w >= width && rect.h >= height) {
+ if (rect.y < minY) {
+ minY = rect.y;
+ multipleAtSameY = false;
+ bestIndex = i;
+ } else if (minY == rect.y) {
+ multipleAtSameY = true;
+ }
+ }
+ }
+ }
+
+ if (!multipleAtSameY) {
+ return bestIndex;
+ }
+
+ bestIndex = -1;
+
+ // Pick a rectangle. This currently tries to find the rectangle whose shortest
+ // side most closely matches the placed rectangle's size.
+ // Attempt to find the best short side fit
+ int bestShortDistance = Integer.MAX_VALUE;
+ int bestLongDistance = Integer.MAX_VALUE;
+
+ for (int i = 0, n = mSpace.size(); i < n; i++) {
+ Rect rect = mSpace.get(i);
+ if (rect.y != minY) { // Only comparing elements at same y
+ continue;
+ }
+ if (rect.w >= width && rect.h >= height) {
+ if (width < height) {
+ int distance = rect.w - width;
+ if (distance < bestShortDistance ||
+ distance == bestShortDistance &&
+ (rect.h - height) < bestLongDistance) {
+ bestShortDistance = distance;
+ bestLongDistance = rect.h - height;
+ bestIndex = i;
+ }
+ } else {
+ int distance = rect.w - width;
+ if (distance < bestShortDistance ||
+ distance == bestShortDistance &&
+ (rect.h - height) < bestLongDistance) {
+ bestShortDistance = distance;
+ bestLongDistance = rect.h - height;
+ bestIndex = i;
+ }
+ }
+ }
+ }
+
+ return bestIndex;
+ }
+
+ /**
+ * Removes the rectangle at the given index. Since the rectangles are in an
+ * {@link ArrayList}, removing a rectangle in the normal way is slow (it
+ * would involve shifting all elements), but since we don't care about
+ * order, this always swaps the to-be-deleted element to the last position
+ * in the array first, <b>then</b> it deletes it (which should be
+ * immediate).
+ *
+ * @param index the index in the {@link #mSpace} list to remove a rectangle
+ * from
+ */
+ private void removeRect(int index) {
+ assert !mSpace.isEmpty();
+ int lastIndex = mSpace.size() - 1;
+ if (index != lastIndex) {
+ // Swap before remove to make deletion faster since we don't
+ // care about order
+ Rect temp = mSpace.get(index);
+ mSpace.set(index, mSpace.get(lastIndex));
+ mSpace.set(lastIndex, temp);
+ }
+
+ mSpace.remove(lastIndex);
+ }
+
+ /**
+ * Splits the rectangle at the given rectangle index such that it can contain
+ * a rectangle of the given width and height. */
+ private Rect split(int index, int width, int height) {
+ Rect rect = mSpace.get(index);
+ assert rect.w >= width && rect.h >= height : rect;
+
+ Rect r = new Rect(rect);
+ r.w = width;
+ r.h = height;
+
+ // Remove all rectangles that intersect my rectangle
+ for (int i = 0; i < mSpace.size(); i++) {
+ Rect other = mSpace.get(i);
+ if (other.intersects(r)) {
+ removeRect(i);
+ i--;
+ }
+ }
+
+
+ // Split along vertical line x = rect.x + width:
+ // (rect.x,rect.y)
+ // +-------------+-------------------------+
+ // | | |
+ // | | |
+ // | | height |
+ // | | |
+ // | | |
+ // +-------------+ B | rect.h
+ // | width |
+ // | | |
+ // | A |
+ // | | |
+ // | |
+ // +---------------------------------------+
+ // rect.w
+ int remainingHeight = rect.h - height;
+ int remainingWidth = rect.w - width;
+ if (remainingHeight >= mMinHeight) {
+ mSpace.add(new Rect(rect.x, rect.y + height, width, remainingHeight));
+ }
+ if (remainingWidth >= mMinWidth) {
+ mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, rect.h));
+ }
+
+ // Split along horizontal line y = rect.y + height:
+ // +-------------+-------------------------+
+ // | | |
+ // | | height |
+ // | | A |
+ // | | |
+ // | | | rect.h
+ // +-------------+ - - - - - - - - - - - - |
+ // | width |
+ // | |
+ // | B |
+ // | |
+ // | |
+ // +---------------------------------------+
+ // rect.w
+ if (remainingHeight >= mMinHeight) {
+ mSpace.add(new Rect(rect.x, rect.y + height, rect.w, remainingHeight));
+ }
+ if (remainingWidth >= mMinWidth) {
+ mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, height));
+ }
+
+ // Remove redundant rectangles. This is not very efficient.
+ for (int i = 0; i < mSpace.size() - 1; i++) {
+ for (int j = i + 1; j < mSpace.size(); j++) {
+ Rect iRect = mSpace.get(i);
+ Rect jRect = mSpace.get(j);
+ if (jRect.contains(iRect)) {
+ removeRect(i);
+ i--;
+ break;
+ }
+ if (iRect.contains(jRect)) {
+ removeRect(j);
+ j--;
+ }
+ }
+ }
+
+ if (DEBUG) {
+ mAllocated.add(r);
+ dumpImage();
+ }
+
+ return r;
+ }
+
+ // DEBUGGING CODE: Enable with DEBUG
+
+ private List<Rect> mAllocated;
+ private static int sLayoutId;
+ private static int sRectId;
+ private void dumpImage() {
+ if (DEBUG) {
+ int width = 100;
+ int height = 100;
+ for (Rect rect : mSpace) {
+ width = Math.max(width, rect.w);
+ height = Math.max(height, rect.h);
+ }
+ width += 10;
+ height += 10;
+
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = image.createGraphics();
+ g.setColor(Color.BLACK);
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+
+ Color[] colors = new Color[] {
+ Color.blue, Color.cyan,
+ Color.green, Color.magenta, Color.orange,
+ Color.pink, Color.red, Color.white, Color.yellow, Color.darkGray,
+ Color.lightGray, Color.gray,
+ };
+
+ char allocated = 'A';
+ for (Rect rect : mAllocated) {
+ Color color = new Color(0x9FFFFFFF, true);
+ g.setColor(color);
+ g.setBackground(color);
+ g.fillRect(rect.x, rect.y, rect.w, rect.h);
+ g.setColor(Color.WHITE);
+ g.drawRect(rect.x, rect.y, rect.w, rect.h);
+ g.drawString("" + (allocated++),
+ rect.x + rect.w / 2, rect.y + rect.h / 2);
+ }
+
+ int colorIndex = 0;
+ for (Rect rect : mSpace) {
+ Color color = colors[colorIndex];
+ colorIndex = (colorIndex + 1) % colors.length;
+
+ color = new Color(color.getRed(), color.getGreen(), color.getBlue(), 128);
+ g.setColor(color);
+
+ g.fillRect(rect.x, rect.y, rect.w, rect.h);
+ g.setColor(Color.WHITE);
+ g.drawString(Integer.toString(colorIndex),
+ rect.x + rect.w / 2, rect.y + rect.h / 2);
+ }
+
+
+ g.dispose();
+
+ File file = new File("/tmp/layout" + sLayoutId + "_pass" + sRectId + ".png");
+ try {
+ ImageIO.write(image, "PNG", file);
+ System.out.println("Wrote diagnostics image " + file);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ sRectId++;
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java
new file mode 100644
index 000000000..c04061cbd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import java.util.List;
+
+/**
+ * Information for the current alternate selection, i.e. the possible selected items
+ * that are located at the same x/y as the original view, either sibling or parents.
+ */
+/* package */ class CanvasAlternateSelection {
+ private final CanvasViewInfo mOriginatingView;
+ private final List<CanvasViewInfo> mAltViews;
+ private int mIndex;
+
+ /**
+ * Creates a new alternate selection based on the given originating view and the
+ * given list of alternate views. Both cannot be null.
+ */
+ public CanvasAlternateSelection(CanvasViewInfo originatingView, List<CanvasViewInfo> altViews) {
+ assert originatingView != null;
+ assert altViews != null;
+ mOriginatingView = originatingView;
+ mAltViews = altViews;
+ mIndex = altViews.size() - 1;
+ }
+
+ /** Returns the list of alternate views. Cannot be null. */
+ public List<CanvasViewInfo> getAltViews() {
+ return mAltViews;
+ }
+
+ /** Returns the originating view. Cannot be null. */
+ public CanvasViewInfo getOriginatingView() {
+ return mOriginatingView;
+ }
+
+ /**
+ * Returns the current alternate view to select.
+ * Initially this is the top-most view.
+ */
+ public CanvasViewInfo getCurrent() {
+ return mIndex >= 0 ? mAltViews.get(mIndex) : null;
+ }
+
+ /**
+ * Changes the current view to be the next one and then returns it.
+ * This loops through the alternate views.
+ */
+ public CanvasViewInfo getNext() {
+ if (mIndex == 0) {
+ mIndex = mAltViews.size() - 1;
+ } else if (mIndex > 0) {
+ mIndex--;
+ }
+
+ return getCurrent();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java
new file mode 100644
index 000000000..ad5bd52e5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.ScrollBar;
+
+/**
+ * Helper class to convert between control pixel coordinates and canvas coordinates.
+ * Takes care of the zooming and offset of the canvas.
+ */
+public class CanvasTransform {
+ /**
+ * Default margin around the rendered image, reduced
+ * when the contents do not fit.
+ */
+ public static final int DEFAULT_MARGIN = 25;
+
+ /**
+ * The canvas which controls the zooming.
+ */
+ private final LayoutCanvas mCanvas;
+
+ /** Canvas image size (original, before zoom), in pixels. */
+ private int mImgSize;
+
+ /** Full size being scrolled (after zoom), in pixels */
+ private int mFullSize;;
+
+ /** Client size, in pixels. */
+ private int mClientSize;
+
+ /** Left-top offset in client pixel coordinates. */
+ private int mTranslate;
+
+ /** Current margin */
+ private int mMargin = DEFAULT_MARGIN;
+
+ /** Scaling factor, > 0. */
+ private double mScale;
+
+ /** Scrollbar widget. */
+ private ScrollBar mScrollbar;
+
+ public CanvasTransform(LayoutCanvas layoutCanvas, ScrollBar scrollbar) {
+ mCanvas = layoutCanvas;
+ mScrollbar = scrollbar;
+ mScale = 1.0;
+ mTranslate = 0;
+
+ mScrollbar.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // User requested scrolling. Changes translation and redraw canvas.
+ mTranslate = mScrollbar.getSelection();
+ CanvasTransform.this.mCanvas.redraw();
+ }
+ });
+ mScrollbar.setIncrement(20);
+ }
+
+ /**
+ * Sets the new scaling factor. Recomputes scrollbars.
+ * @param scale Scaling factor, > 0.
+ */
+ public void setScale(double scale) {
+ if (mScale != scale) {
+ mScale = scale;
+ resizeScrollbar();
+ }
+ }
+
+ /** Recomputes the scrollbar and view port settings */
+ public void refresh() {
+ resizeScrollbar();
+ }
+
+ /**
+ * Returns current scaling factor.
+ *
+ * @return The current scaling factor
+ */
+ public double getScale() {
+ return mScale;
+ }
+
+ /**
+ * Returns Canvas image size (original, before zoom), in pixels.
+ *
+ * @return Canvas image size (original, before zoom), in pixels
+ */
+ public int getImgSize() {
+ return mImgSize;
+ }
+
+ /**
+ * Returns the scaled image size in pixels.
+ *
+ * @return The scaled image size in pixels.
+ */
+ public int getScaledImgSize() {
+ return (int) (mImgSize * mScale);
+ }
+
+ /**
+ * Changes the size of the canvas image and the client size. Recomputes
+ * scrollbars.
+ *
+ * @param imgSize the size of the image being scaled
+ * @param fullSize the size of the full view area being scrolled
+ * @param clientSize the size of the view port
+ */
+ public void setSize(int imgSize, int fullSize, int clientSize) {
+ mImgSize = imgSize;
+ mFullSize = fullSize;
+ mClientSize = clientSize;
+ mScrollbar.setPageIncrement(clientSize);
+ resizeScrollbar();
+ }
+
+ private void resizeScrollbar() {
+ // scaled image size
+ int sx = (int) (mScale * mFullSize);
+
+ // Adjust margin such that for zoomed out views
+ // we don't waste space (unless the viewport is
+ // large enough to accommodate it)
+ int delta = mClientSize - sx;
+ if (delta < 0) {
+ mMargin = 0;
+ } else if (delta < 2 * DEFAULT_MARGIN) {
+ mMargin = delta / 2;
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ if (imageOverlay != null && imageOverlay.getShowDropShadow()
+ && delta >= SHADOW_SIZE / 2) {
+ mMargin -= SHADOW_SIZE / 2;
+ // Add a little padding on the top too, if there's room. The shadow assets
+ // include enough padding on the bottom to not make this look clipped.
+ if (mMargin < 4) {
+ mMargin += 4;
+ }
+ }
+ } else {
+ mMargin = DEFAULT_MARGIN;
+ }
+
+ if (mCanvas.getPreviewManager().hasPreviews()) {
+ // Make more room for the previews
+ mMargin = 2;
+ }
+
+ // actual client area is always reduced by the margins
+ int cx = mClientSize - 2 * mMargin;
+
+ if (sx < cx) {
+ mTranslate = 0;
+ mScrollbar.setEnabled(false);
+ } else {
+ mScrollbar.setEnabled(true);
+
+ int selection = mScrollbar.getSelection();
+ int thumb = cx;
+ int maximum = sx;
+
+ if (selection + thumb > maximum) {
+ selection = maximum - thumb;
+ if (selection < 0) {
+ selection = 0;
+ }
+ }
+
+ mScrollbar.setValues(selection, mScrollbar.getMinimum(), maximum, thumb, mScrollbar
+ .getIncrement(), mScrollbar.getPageIncrement());
+
+ mTranslate = selection;
+ }
+ }
+
+ public int getMargin() {
+ return mMargin;
+ }
+
+ public int translate(int canvasX) {
+ return mMargin - mTranslate + (int) (mScale * canvasX);
+ }
+
+ public int scale(int canwasW) {
+ return (int) (mScale * canwasW);
+ }
+
+ public int inverseTranslate(int screenX) {
+ return (int) ((screenX - mMargin + mTranslate) / mScale);
+ }
+
+ public int inverseScale(int canwasW) {
+ return (int) (canwasW / mScale);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
new file mode 100644
index 000000000..03c6c3926
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
@@ -0,0 +1,1178 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.MergeCookie;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.eclipse.ui.views.properties.IPropertySource;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps a {@link ViewInfo} in a structure more adapted to our needs.
+ * The only large difference is that we keep both the original bounds of the view info
+ * and we pre-compute the selection bounds which are absolute to the rendered image
+ * (whereas the original bounds are relative to the parent view.)
+ * <p/>
+ * Each view also knows its parent and children.
+ * <p/>
+ * We can't alter {@link ViewInfo} as it is part of the LayoutBridge and needs to
+ * have a fixed API.
+ * <p/>
+ * The view info also implements {@link IPropertySource}, which enables a linked
+ * {@link IPropertySheetPage} to display the attributes of the selected element.
+ * This class actually delegates handling of {@link IPropertySource} to the underlying
+ * {@link UiViewElementNode}, if any.
+ */
+public class CanvasViewInfo implements IPropertySource {
+
+ /**
+ * Minimal size of the selection, in case an empty view or layout is selected.
+ */
+ public static final int SELECTION_MIN_SIZE = 6;
+
+ private final Rectangle mAbsRect;
+ private final Rectangle mSelectionRect;
+ private final String mName;
+ private final Object mViewObject;
+ private final UiViewElementNode mUiViewNode;
+ private CanvasViewInfo mParent;
+ private ViewInfo mViewInfo;
+ private final List<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>();
+
+ /**
+ * Is this view info an individually exploded view? This is the case for views
+ * that were specially inflated by the {@link UiElementPullParser} and assigned
+ * fixed padding because they were invisible and somebody requested visibility.
+ */
+ private boolean mExploded;
+
+ /**
+ * Node sibling. This is usually null, but it's possible for a single node in the
+ * model to have <b>multiple</b> separate views in the canvas, for example
+ * when you {@code <include>} a view that has multiple widgets inside a
+ * {@code <merge>} tag. In this case, all the views have the same node model,
+ * the include tag, and selecting the include should highlight all the separate
+ * views that are linked to this node. That's what this field is all about: it is
+ * a <b>circular</b> list of all the siblings that share the same node.
+ */
+ private List<CanvasViewInfo> mNodeSiblings;
+
+ /**
+ * Constructs a {@link CanvasViewInfo} initialized with the given initial values.
+ */
+ private CanvasViewInfo(CanvasViewInfo parent, String name,
+ Object viewObject, UiViewElementNode node, Rectangle absRect,
+ Rectangle selectionRect, ViewInfo viewInfo) {
+ mParent = parent;
+ mName = name;
+ mViewObject = viewObject;
+ mViewInfo = viewInfo;
+ mUiViewNode = node;
+ mAbsRect = absRect;
+ mSelectionRect = selectionRect;
+ }
+
+ /**
+ * Returns the original {@link ViewInfo} bounds in absolute coordinates
+ * over the whole graphic.
+ *
+ * @return the bounding box in absolute coordinates
+ */
+ @NonNull
+ public Rectangle getAbsRect() {
+ return mAbsRect;
+ }
+
+ /**
+ * Returns the absolute selection bounds of the view info as a rectangle.
+ * The selection bounds will always have a size greater or equal to
+ * {@link #SELECTION_MIN_SIZE}.
+ * The width/height is inclusive (i.e. width = right-left-1).
+ * This is in absolute "screen" coordinates (relative to the rendered bitmap).
+ *
+ * @return the absolute selection bounds
+ */
+ @NonNull
+ public Rectangle getSelectionRect() {
+ return mSelectionRect;
+ }
+
+ /**
+ * Returns the view node. Could be null, although unlikely.
+ * @return An {@link UiViewElementNode} that uniquely identifies the object in the XML model.
+ * @see ViewInfo#getCookie()
+ */
+ @Nullable
+ public UiViewElementNode getUiViewNode() {
+ return mUiViewNode;
+ }
+
+ /**
+ * Returns the parent {@link CanvasViewInfo}.
+ * It is null for the root and non-null for children.
+ *
+ * @return the parent {@link CanvasViewInfo}, which can be null
+ */
+ @Nullable
+ public CanvasViewInfo getParent() {
+ return mParent;
+ }
+
+ /**
+ * Returns the list of children of this {@link CanvasViewInfo}.
+ * The list is never null. It can be empty.
+ * By contract, this.getChildren().get(0..n-1).getParent() == this.
+ *
+ * @return the children, never null
+ */
+ @NonNull
+ public List<CanvasViewInfo> getChildren() {
+ return mChildren;
+ }
+
+ /**
+ * For nodes that have multiple views rendered from a single node, such as the
+ * children of a {@code <merge>} tag included into a separate layout, return the
+ * "primary" view, the first view that is rendered
+ */
+ @Nullable
+ private CanvasViewInfo getPrimaryNodeSibling() {
+ if (mNodeSiblings == null || mNodeSiblings.size() == 0) {
+ return null;
+ }
+
+ return mNodeSiblings.get(0);
+ }
+
+ /**
+ * Returns true if this view represents one view of many linked to a single node, and
+ * where this is the primary view. The primary view is the one that will be shown
+ * in the outline for example (since we only show nodes, not views, in the outline,
+ * and therefore don't want repetitions when a view has more than one view info.)
+ *
+ * @return true if this is the primary view among more than one linked to a single
+ * node
+ */
+ private boolean isPrimaryNodeSibling() {
+ return getPrimaryNodeSibling() == this;
+ }
+
+ /**
+ * Returns the list of node sibling of this view (which <b>will include this
+ * view</b>). For most views this is going to be null, but for views that share a
+ * single node (such as widgets inside a {@code <merge>} tag included into another
+ * layout), this will provide all the views that correspond to the node.
+ *
+ * @return a non-empty list of siblings (including this), or null
+ */
+ @Nullable
+ public List<CanvasViewInfo> getNodeSiblings() {
+ return mNodeSiblings;
+ }
+
+ /**
+ * Returns all the children of the canvas view info where each child corresponds to a
+ * unique node that the user can see and select. This is intended for use by the
+ * outline for example, where only the actual nodes are displayed, not the views
+ * themselves.
+ * <p>
+ * Most views have their own nodes, so this is generally the same as
+ * {@link #getChildren}, except in the case where you for example include a view that
+ * has multiple widgets inside a {@code <merge>} tag, where all these widgets have the
+ * same node (the {@code <merge>} tag).
+ *
+ * @return list of {@link CanvasViewInfo} objects that are children of this view,
+ * never null
+ */
+ @NonNull
+ public List<CanvasViewInfo> getUniqueChildren() {
+ boolean haveHidden = false;
+
+ for (CanvasViewInfo info : mChildren) {
+ if (info.mNodeSiblings != null) {
+ // We have secondary children; must create a new collection containing
+ // only non-secondary children
+ List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>();
+ for (CanvasViewInfo vi : mChildren) {
+ if (vi.mNodeSiblings == null) {
+ children.add(vi);
+ } else if (vi.isPrimaryNodeSibling()) {
+ children.add(vi);
+ }
+ }
+ return children;
+ }
+
+ haveHidden |= info.isHidden();
+ }
+
+ if (haveHidden) {
+ List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(mChildren.size());
+ for (CanvasViewInfo vi : mChildren) {
+ if (!vi.isHidden()) {
+ children.add(vi);
+ }
+ }
+
+ return children;
+ }
+
+ return mChildren;
+ }
+
+ /**
+ * Returns true if the specific {@link CanvasViewInfo} is a parent
+ * of this {@link CanvasViewInfo}. It can be a direct parent or any
+ * grand-parent higher in the hierarchy.
+ *
+ * @param potentialParent the view info to check
+ * @return true if the given info is a parent of this view
+ */
+ public boolean isParent(@NonNull CanvasViewInfo potentialParent) {
+ CanvasViewInfo p = mParent;
+ while (p != null) {
+ if (p == potentialParent) {
+ return true;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the name of the {@link CanvasViewInfo}.
+ * Could be null, although unlikely.
+ * Experience shows this is the full qualified Java name of the View.
+ * TODO: Rename this method to getFqcn.
+ *
+ * @return the name of the view info
+ *
+ * @see ViewInfo#getClassName()
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the View object associated with the {@link CanvasViewInfo}.
+ * @return the view object or null.
+ */
+ @Nullable
+ public Object getViewObject() {
+ return mViewObject;
+ }
+
+ /**
+ * Returns the baseline of this object, or -1 if it does not support a baseline
+ *
+ * @return the baseline or -1
+ */
+ public int getBaseline() {
+ if (mViewInfo != null) {
+ int baseline = mViewInfo.getBaseLine();
+ if (baseline != Integer.MIN_VALUE) {
+ return baseline;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the {@link Margins} for this {@link CanvasViewInfo}
+ *
+ * @return the {@link Margins} for this {@link CanvasViewInfo}
+ */
+ @Nullable
+ public Margins getMargins() {
+ if (mViewInfo != null) {
+ int leftMargin = mViewInfo.getLeftMargin();
+ int topMargin = mViewInfo.getTopMargin();
+ int rightMargin = mViewInfo.getRightMargin();
+ int bottomMargin = mViewInfo.getBottomMargin();
+ return new Margins(
+ leftMargin != Integer.MIN_VALUE ? leftMargin : 0,
+ rightMargin != Integer.MIN_VALUE ? rightMargin : 0,
+ topMargin != Integer.MIN_VALUE ? topMargin : 0,
+ bottomMargin != Integer.MIN_VALUE ? bottomMargin : 0
+ );
+ }
+
+ return null;
+ }
+
+ // ---- Implementation of IPropertySource
+ // TODO: Get rid of this once the old propertysheet implementation is fully gone
+
+ @Override
+ public Object getEditableValue() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getEditableValue();
+ }
+ return null;
+ }
+
+ @Override
+ public IPropertyDescriptor[] getPropertyDescriptors() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getPropertyDescriptors();
+ }
+ return null;
+ }
+
+ @Override
+ public Object getPropertyValue(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getPropertyValue(id);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isPropertySet(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).isPropertySet(id);
+ }
+ return false;
+ }
+
+ @Override
+ public void resetPropertyValue(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ ((IPropertySource) uiView).resetPropertyValue(id);
+ }
+ }
+
+ @Override
+ public void setPropertyValue(Object id, Object value) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ ((IPropertySource) uiView).setPropertyValue(id, value);
+ }
+ }
+
+ /**
+ * Returns the XML node corresponding to this info, or null if there is no
+ * such XML node.
+ *
+ * @return The XML node corresponding to this info object, or null
+ */
+ @Nullable
+ public Node getXmlNode() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return uiView.getXmlNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true iff this view info corresponds to a root element.
+ *
+ * @return True iff this is a root view info.
+ */
+ public boolean isRoot() {
+ // Select the visual element -- unless it's the root.
+ // The root element is the one whose GRAND parent
+ // is null (because the parent will be a -document-
+ // node).
+
+ // Special case: a gesture overlay is sometimes added as the root, but for all intents
+ // and purposes it is its layout child that is the real root so treat that one as the
+ // root as well (such that the whole layout canvas does not highlight as part of hovers
+ // etc)
+ if (mParent != null
+ && mParent.mName.endsWith(GESTURE_OVERLAY_VIEW)
+ && mParent.isRoot()
+ && mParent.mChildren.size() == 1) {
+ return true;
+ }
+
+ return mUiViewNode == null || mUiViewNode.getUiParent() == null ||
+ mUiViewNode.getUiParent().getUiParent() == null;
+ }
+
+ /**
+ * Returns true if this {@link CanvasViewInfo} represents an invisible widget that
+ * should be highlighted when selected. This is the case for any layout that is less than the minimum
+ * threshold ({@link #SELECTION_MIN_SIZE}), or any other view that has -0- bounds.
+ *
+ * @return True if this is a tiny layout or invisible view
+ */
+ public boolean isInvisible() {
+ if (isHidden()) {
+ // Don't expand and highlight hidden widgets
+ return false;
+ }
+
+ if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) {
+ return mUiViewNode != null && (mUiViewNode.getDescriptor().hasChildren() ||
+ mAbsRect.width <= 0 || mAbsRect.height <= 0);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this {@link CanvasViewInfo} represents a widget that should be
+ * hidden, such as a {@code <Space>} which are typically not manipulated by the user
+ * through dragging etc.
+ *
+ * @return true if this is a hidden view
+ */
+ public boolean isHidden() {
+ if (GridLayoutRule.sDebugGridLayout) {
+ return false;
+ }
+
+ return FQCN_SPACE.equals(mName) || FQCN_SPACE_V7.equals(mName);
+ }
+
+ /**
+ * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to
+ * make it visible during selection or dragging? Note that this is NOT considered to
+ * be the case in the explode-all-views mode where all nodes have their padding
+ * increased; it's only used for views that individually exploded because they were
+ * requested visible and they returned true for {@link #isInvisible()}.
+ *
+ * @return True if this is an exploded node.
+ */
+ public boolean isExploded() {
+ return mExploded;
+ }
+
+ /**
+ * Mark this {@link CanvasViewInfo} as having been exploded or not. See the
+ * {@link #isExploded()} method for details on what this property means.
+ *
+ * @param exploded New value of the exploded property to mark this info with.
+ */
+ void setExploded(boolean exploded) {
+ mExploded = exploded;
+ }
+
+ /**
+ * Returns the info represented as a {@link SimpleElement}.
+ *
+ * @return A {@link SimpleElement} wrapping this info.
+ */
+ @NonNull
+ SimpleElement toSimpleElement() {
+
+ UiViewElementNode uiNode = getUiViewNode();
+
+ String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
+ String parentFqcn = null;
+ Rect bounds = SwtUtils.toRect(getAbsRect());
+ Rect parentBounds = null;
+
+ UiElementNode uiParent = uiNode.getUiParent();
+ if (uiParent != null) {
+ parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
+ }
+ if (getParent() != null) {
+ parentBounds = SwtUtils.toRect(getParent().getAbsRect());
+ }
+
+ SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
+
+ for (UiAttributeNode attr : uiNode.getAllUiAttributes()) {
+ String value = attr.getCurrentValue();
+ if (value != null && value.length() > 0) {
+ AttributeDescriptor attrDesc = attr.getDescriptor();
+ SimpleAttribute a = new SimpleAttribute(
+ attrDesc.getNamespaceUri(),
+ attrDesc.getXmlLocalName(),
+ value);
+ e.addAttribute(a);
+ }
+ }
+
+ for (CanvasViewInfo childVi : getChildren()) {
+ SimpleElement e2 = childVi.toSimpleElement();
+ if (e2 != null) {
+ e.addInnerElement(e2);
+ }
+ }
+
+ return e;
+ }
+
+ /**
+ * Returns the layout url attribute value for the closest surrounding include or
+ * fragment element parent, or null if this {@link CanvasViewInfo} is not rendered as
+ * part of an include or fragment tag.
+ *
+ * @return the layout url attribute value for the surrounding include tag, or null if
+ * not applicable
+ */
+ @Nullable
+ public String getIncludeUrl() {
+ CanvasViewInfo curr = this;
+ while (curr != null) {
+ if (curr.mUiViewNode != null) {
+ Node node = curr.mUiViewNode.getXmlNode();
+ if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
+ String nodeName = node.getNodeName();
+ if (node.getNamespaceURI() == null
+ && SdkConstants.VIEW_INCLUDE.equals(nodeName)) {
+ // Note: the layout attribute is NOT in the Android namespace
+ Element element = (Element) node;
+ String url = element.getAttribute(SdkConstants.ATTR_LAYOUT);
+ if (url.length() > 0) {
+ return url;
+ }
+ } else if (SdkConstants.VIEW_FRAGMENT.equals(nodeName)) {
+ String url = FragmentMenu.getFragmentLayout(node);
+ if (url != null) {
+ return url;
+ }
+ }
+ }
+ }
+ curr = curr.mParent;
+ }
+
+ return null;
+ }
+
+ /** Adds the given {@link CanvasViewInfo} as a new last child of this view */
+ private void addChild(@NonNull CanvasViewInfo child) {
+ mChildren.add(child);
+ }
+
+ /** Adds the given {@link CanvasViewInfo} as a child at the given index */
+ private void addChildAt(int index, @NonNull CanvasViewInfo child) {
+ mChildren.add(index, child);
+ }
+
+ /**
+ * Removes the given {@link CanvasViewInfo} from the child list of this view, and
+ * returns true if it was successfully removed
+ *
+ * @param child the child to be removed
+ * @return true if it was a child and was removed
+ */
+ public boolean removeChild(@NonNull CanvasViewInfo child) {
+ return mChildren.remove(child);
+ }
+
+ @Override
+ public String toString() {
+ return "CanvasViewInfo [name=" + mName + ", node=" + mUiViewNode + "]";
+ }
+
+ // ---- Factory functionality ----
+
+ /**
+ * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo}
+ * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo}
+ * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo}
+ * objects for {@link ViewInfo} objects that contain a reference to an
+ * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML
+ * file for this layout file. This is not always the case, such as in the following
+ * scenarios:
+ * <ul>
+ * <li>we link to other layouts with {@code <include>}
+ * <li>the current view is rendered within another view ("Show Included In") such that
+ * the outer file does not correspond to elements in the current included XML layout
+ * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there
+ * is no reference to the {@code <include>} tag
+ * <li>with the {@code <merge>} tag we don't get a reference to the corresponding
+ * element
+ * <ul>
+ * <p>
+ * This method will build up a set of {@link CanvasViewInfo} that corresponds to the
+ * actual <b>selectable</b> views (which are also shown in the Outline).
+ *
+ * @param layoutlib5 if true, the {@link ViewInfo} hierarchy was created by layoutlib
+ * version 5 or higher, which means this algorithm can make certain assumptions
+ * (for example that {@code <merge>} siblings will provide {@link MergeCookie}
+ * references, so we don't have to search for them.)
+ * @param root the root {@link ViewInfo} to build from
+ * @return a {@link CanvasViewInfo} hierarchy
+ */
+ @NonNull
+ public static Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root, boolean layoutlib5) {
+ return new Builder(layoutlib5).create(root);
+ }
+
+ /** Builder object which walks over a tree of {@link ViewInfo} objects and builds
+ * up a corresponding {@link CanvasViewInfo} hierarchy. */
+ private static class Builder {
+ public Builder(boolean layoutlib5) {
+ mLayoutLib5 = layoutlib5;
+ }
+
+ /**
+ * The mapping from nodes that have a {@code <merge>} as a parent in the node
+ * model to their corresponding views
+ */
+ private Map<UiViewElementNode, List<CanvasViewInfo>> mMergeNodeMap;
+
+ /**
+ * Whether the ViewInfos are provided by a layout library that is version 5 or
+ * later, since that will allow us to take several shortcuts
+ */
+ private boolean mLayoutLib5;
+
+ /**
+ * Creates a hierarchy of {@link CanvasViewInfo} objects and merge bounding
+ * rectangles from the given {@link ViewInfo} hierarchy
+ */
+ private Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root) {
+ Object cookie = root.getCookie();
+ if (cookie == null) {
+ // Special case: If the root-most view does not have a view cookie,
+ // then we are rendering some outer layout surrounding this layout, and in
+ // that case we must search down the hierarchy for the (possibly multiple)
+ // sub-roots that correspond to elements in this layout, and place them inside
+ // an outer view that has no node. In the outline this item will be used to
+ // show the inclusion-context.
+ CanvasViewInfo rootView = createView(null, root, 0, 0);
+ addKeyedSubtrees(rootView, root, 0, 0);
+
+ List<Rectangle> includedBounds = new ArrayList<Rectangle>();
+ for (CanvasViewInfo vi : rootView.getChildren()) {
+ if (vi.getNodeSiblings() == null || vi.isPrimaryNodeSibling()) {
+ includedBounds.add(vi.getAbsRect());
+ }
+ }
+
+ // There are <merge> nodes here; see if we can insert it into the hierarchy
+ if (mMergeNodeMap != null) {
+ // Locate all the nodes that have a <merge> as a parent in the node model,
+ // and where the view sits at the top level inside the include-context node.
+ UiViewElementNode merge = null;
+ List<CanvasViewInfo> merged = new ArrayList<CanvasViewInfo>();
+ for (Map.Entry<UiViewElementNode, List<CanvasViewInfo>> entry : mMergeNodeMap
+ .entrySet()) {
+ UiViewElementNode node = entry.getKey();
+ if (!hasMergeParent(node)) {
+ continue;
+ }
+ List<CanvasViewInfo> views = entry.getValue();
+ assert views.size() > 0;
+ CanvasViewInfo view = views.get(0); // primary
+ if (view.getParent() != rootView) {
+ continue;
+ }
+ UiElementNode parent = node.getUiParent();
+ if (merge != null && parent != merge) {
+ continue;
+ }
+ merge = (UiViewElementNode) parent;
+ merged.add(view);
+ }
+ if (merged.size() > 0) {
+ // Compute a bounding box for the merged views
+ Rectangle absRect = null;
+ for (CanvasViewInfo child : merged) {
+ Rectangle rect = child.getAbsRect();
+ if (absRect == null) {
+ absRect = rect;
+ } else {
+ absRect = absRect.union(rect);
+ }
+ }
+
+ CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null,
+ merge, absRect, absRect, null /* viewInfo */);
+ for (CanvasViewInfo view : merged) {
+ if (rootView.removeChild(view)) {
+ mergeView.addChild(view);
+ }
+ }
+ rootView.addChild(mergeView);
+ }
+ }
+
+ return Pair.of(rootView, includedBounds);
+ } else {
+ // We have a view key at the top, so just go and create {@link CanvasViewInfo}
+ // objects for each {@link ViewInfo} until we run into a null key.
+ CanvasViewInfo rootView = addKeyedSubtrees(null, root, 0, 0);
+
+ // Special case: look to see if the root element is really a <merge>, and if so,
+ // manufacture a view for it such that we can target this root element
+ // in drag & drop operations, such that we can show it in the outline, etc
+ if (rootView != null && hasMergeParent(rootView.getUiViewNode())) {
+ CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null,
+ (UiViewElementNode) rootView.getUiViewNode().getUiParent(),
+ rootView.getAbsRect(), rootView.getSelectionRect(),
+ null /* viewInfo */);
+ // Insert the <merge> as the new real root
+ rootView.mParent = merge;
+ merge.addChild(rootView);
+ rootView = merge;
+ }
+
+ return Pair.of(rootView, null);
+ }
+ }
+
+ private boolean hasMergeParent(UiViewElementNode rootNode) {
+ UiElementNode rootParent = rootNode.getUiParent();
+ return (rootParent instanceof UiViewElementNode
+ && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName()));
+ }
+
+ /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */
+ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+ int parentY) {
+ Object cookie = root.getCookie();
+ UiViewElementNode node = null;
+ if (cookie instanceof UiViewElementNode) {
+ node = (UiViewElementNode) cookie;
+ } else if (cookie instanceof MergeCookie) {
+ cookie = ((MergeCookie) cookie).getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ node = (UiViewElementNode) cookie;
+ CanvasViewInfo view = createView(parent, root, parentX, parentY, node);
+ if (root.getCookie() instanceof MergeCookie && view.mNodeSiblings == null) {
+ List<CanvasViewInfo> v = mMergeNodeMap == null ?
+ null : mMergeNodeMap.get(node);
+ if (v != null) {
+ v.add(view);
+ } else {
+ v = new ArrayList<CanvasViewInfo>();
+ v.add(view);
+ if (mMergeNodeMap == null) {
+ mMergeNodeMap =
+ new HashMap<UiViewElementNode, List<CanvasViewInfo>>();
+ }
+ mMergeNodeMap.put(node, v);
+ }
+ view.mNodeSiblings = v;
+ }
+
+ return view;
+ }
+ }
+
+ return createView(parent, root, parentX, parentY, node);
+ }
+
+ /**
+ * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse.
+ * This method specifies an explicit {@link UiViewElementNode} to use rather than
+ * relying on the view cookie in the info object.
+ */
+ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+ int parentY, UiViewElementNode node) {
+
+ int x = root.getLeft();
+ int y = root.getTop();
+ int w = root.getRight() - x;
+ int h = root.getBottom() - y;
+
+ x += parentX;
+ y += parentY;
+
+ Rectangle absRect = new Rectangle(x, y, w - 1, h - 1);
+
+ if (w < SELECTION_MIN_SIZE) {
+ int d = (SELECTION_MIN_SIZE - w) / 2;
+ x -= d;
+ w += SELECTION_MIN_SIZE - w;
+ }
+
+ if (h < SELECTION_MIN_SIZE) {
+ int d = (SELECTION_MIN_SIZE - h) / 2;
+ y -= d;
+ h += SELECTION_MIN_SIZE - h;
+ }
+
+ Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1);
+
+ return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node,
+ absRect, selectionRect, root);
+ }
+
+ /** Create a subtree recursively until you run out of keys */
+ private CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo,
+ int parentX, int parentY) {
+ assert viewInfo.getCookie() != null;
+
+ CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY);
+ // Bug workaround: Ensure that we never have a child node identical
+ // to its parent node: this can happen for example when rendering a
+ // ZoomControls view where the merge cookies point to the parent.
+ if (parent != null && view.mUiViewNode == parent.mUiViewNode) {
+ return null;
+ }
+
+ // Process children:
+ parentX += viewInfo.getLeft();
+ parentY += viewInfo.getTop();
+
+ List<ViewInfo> children = viewInfo.getChildren();
+
+ if (mLayoutLib5) {
+ for (ViewInfo child : children) {
+ Object cookie = child.getCookie();
+ if (cookie instanceof UiViewElementNode || cookie instanceof MergeCookie) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ if (childView != null) {
+ view.addChild(childView);
+ }
+ } // else: null cookies, adapter item references, etc: No child views.
+ }
+
+ return view;
+ }
+
+ // See if we have any missing keys at this level
+ int missingNodes = 0;
+ int mergeNodes = 0;
+ for (ViewInfo child : children) {
+ // Only use children which have a ViewKey of the correct type.
+ // We can't interact with those when they have a null key or
+ // an incompatible type.
+ Object cookie = child.getCookie();
+ if (!(cookie instanceof UiViewElementNode)) {
+ if (cookie instanceof MergeCookie) {
+ mergeNodes++;
+ } else {
+ missingNodes++;
+ }
+ }
+ }
+
+ if (missingNodes == 0 && mergeNodes == 0) {
+ // No missing nodes; this is the normal case, and we can just continue to
+ // recursively add our children
+ for (ViewInfo child : children) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ view.addChild(childView);
+ }
+
+ // TBD: Emit placeholder views for keys that have no views?
+ } else {
+ // We don't have keys for one or more of the ViewInfos. There are many
+ // possible causes: we are on an SDK platform that does not support
+ // embedded_layout rendering, or we are including a view with a <merge>
+ // as the root element.
+
+ UiViewElementNode uiViewNode = view.getUiViewNode();
+ String containerName = uiViewNode != null
+ ? uiViewNode.getDescriptor().getXmlLocalName() : ""; //$NON-NLS-1$
+ if (containerName.equals(SdkConstants.VIEW_INCLUDE)) {
+ // This is expected -- we don't WANT to get node keys for the content
+ // of an include since it's in a different file and should be treated
+ // as a single unit that cannot be edited (hence, no CanvasViewInfo
+ // children)
+ } else {
+ // We are getting children with null keys where we don't expect it;
+ // this usually means that we are dealing with an Android platform
+ // that does not support {@link Capability#EMBEDDED_LAYOUT}, or
+ // that there are <merge> tags which are doing surprising things
+ // to the view hierarchy
+ LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>();
+ if (uiViewNode != null) {
+ for (UiElementNode child : uiViewNode.getUiChildren()) {
+ if (child instanceof UiViewElementNode) {
+ unused.addLast((UiViewElementNode) child);
+ }
+ }
+ }
+ for (ViewInfo child : children) {
+ Object cookie = child.getCookie();
+ if (mergeNodes > 0 && cookie instanceof MergeCookie) {
+ cookie = ((MergeCookie) cookie).getCookie();
+ }
+ if (cookie != null) {
+ unused.remove(cookie);
+ }
+ }
+
+ if (unused.size() > 0 || mergeNodes > 0) {
+ if (unused.size() == missingNodes) {
+ // The number of unmatched elements and ViewInfos are identical;
+ // it's very likely that they match one to one, so just use these
+ for (ViewInfo child : children) {
+ if (child.getCookie() == null) {
+ // Only create a flat (non-recursive) view
+ CanvasViewInfo childView = createView(view, child, parentX,
+ parentY, unused.removeFirst());
+ view.addChild(childView);
+ } else {
+ CanvasViewInfo childView = createSubtree(view, child, parentX,
+ parentY);
+ view.addChild(childView);
+ }
+ }
+ } else {
+ // We have an uneven match. In this case we might be dealing
+ // with <merge> etc.
+ // We have no way to associate elements back with the
+ // corresponding <include> tags if there are more than one of
+ // them. That's not a huge tragedy since visually you are not
+ // allowed to edit these anyway; we just need to make a visual
+ // block for these for selection and outline purposes.
+ addMismatched(view, parentX, parentY, children, unused);
+ }
+ } else {
+ // No unused keys, but there are views without keys.
+ // We can't represent these since all views must have node keys
+ // such that you can operate on them. Just ignore these.
+ for (ViewInfo child : children) {
+ if (child.getCookie() != null) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ view.addChild(childView);
+ }
+ }
+ }
+ }
+ }
+
+ return view;
+ }
+
+ /**
+ * We have various {@link ViewInfo} children with null keys, and/or nodes in
+ * the corresponding UI model that are not referenced by any of the {@link ViewInfo}
+ * objects. This method attempts to account for this, by matching the views in
+ * the right order.
+ */
+ private void addMismatched(CanvasViewInfo parentView, int parentX, int parentY,
+ List<ViewInfo> children, LinkedList<UiViewElementNode> unused) {
+ UiViewElementNode afterNode = null;
+ UiViewElementNode beforeNode = null;
+ // We have one important clue we can use when matching unused nodes
+ // with views: if we have a view V1 with node N1, and a view V2 with node N2,
+ // then we can only match unknown node UN with unknown node UV if
+ // V1 < UV < V2 and N1 < UN < N2.
+ // We can use these constraints to do the matching, for example by
+ // a simple DAG traversal. However, since the number of unmatched nodes
+ // will typically be very small, we'll just do a simple algorithm here
+ // which checks forwards/backwards whether a match is valid.
+ for (int index = 0, size = children.size(); index < size; index++) {
+ ViewInfo child = children.get(index);
+ if (child.getCookie() != null) {
+ CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY);
+ if (childView != null) {
+ parentView.addChild(childView);
+ }
+ if (child.getCookie() instanceof UiViewElementNode) {
+ afterNode = (UiViewElementNode) child.getCookie();
+ }
+ } else {
+ beforeNode = nextViewNode(children, index);
+
+ // Find first eligible node from unused
+ // TOD: What if there are more eligible? We need to process ALL views
+ // and all nodes in one go here
+
+ UiViewElementNode matching = null;
+ for (UiViewElementNode candidate : unused) {
+ if (afterNode == null || isAfter(afterNode, candidate)) {
+ if (beforeNode == null || isBefore(beforeNode, candidate)) {
+ matching = candidate;
+ break;
+ }
+ }
+ }
+
+ if (matching != null) {
+ unused.remove(matching);
+ CanvasViewInfo childView = createView(parentView, child, parentX, parentY,
+ matching);
+ parentView.addChild(childView);
+ afterNode = matching;
+ } else {
+ // We have no node for the view -- what do we do??
+ // Nothing - we only represent stuff in the outline that is in the
+ // source model, not in the render
+ }
+ }
+ }
+
+ // Add zero-bounded boxes for all remaining nodes since they need to show
+ // up in the outline, need to be selectable so you can press Delete, etc.
+ if (unused.size() > 0) {
+ Map<UiViewElementNode, Integer> rankMap =
+ new HashMap<UiViewElementNode, Integer>();
+ Map<UiViewElementNode, CanvasViewInfo> infoMap =
+ new HashMap<UiViewElementNode, CanvasViewInfo>();
+ UiElementNode parent = unused.get(0).getUiParent();
+ if (parent != null) {
+ int index = 0;
+ for (UiElementNode child : parent.getUiChildren()) {
+ UiViewElementNode node = (UiViewElementNode) child;
+ rankMap.put(node, index++);
+ }
+ for (CanvasViewInfo child : parentView.getChildren()) {
+ infoMap.put(child.getUiViewNode(), child);
+ }
+ List<Integer> usedIndexes = new ArrayList<Integer>();
+ for (UiViewElementNode node : unused) {
+ Integer rank = rankMap.get(node);
+ if (rank != null) {
+ usedIndexes.add(rank);
+ }
+ }
+ Collections.sort(usedIndexes);
+ for (int i = usedIndexes.size() - 1; i >= 0; i--) {
+ Integer rank = usedIndexes.get(i);
+ UiViewElementNode found = null;
+ for (UiViewElementNode node : unused) {
+ if (rankMap.get(node) == rank) {
+ found = node;
+ break;
+ }
+ }
+ if (found != null) {
+ Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
+ String name = found.getDescriptor().getXmlLocalName();
+ CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found,
+ absRect, absRect, null /* viewInfo */);
+ // Find corresponding index in the parent view
+ List<CanvasViewInfo> siblings = parentView.getChildren();
+ int insertPosition = siblings.size();
+ for (int j = siblings.size() - 1; j >= 0; j--) {
+ CanvasViewInfo sibling = siblings.get(j);
+ UiViewElementNode siblingNode = sibling.getUiViewNode();
+ if (siblingNode != null) {
+ Integer siblingRank = rankMap.get(siblingNode);
+ if (siblingRank != null && siblingRank < rank) {
+ insertPosition = j + 1;
+ break;
+ }
+ }
+ }
+ parentView.addChildAt(insertPosition, v);
+ unused.remove(found);
+ }
+ }
+ }
+ // Add in any remaining
+ for (UiViewElementNode node : unused) {
+ Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
+ String name = node.getDescriptor().getXmlLocalName();
+ CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, node, absRect,
+ absRect, null /* viewInfo */);
+ parentView.addChild(v);
+ }
+ }
+ }
+
+ private boolean isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate) {
+ UiElementNode parent = candidate.getUiParent();
+ if (parent != null) {
+ for (UiElementNode sibling : parent.getUiChildren()) {
+ if (sibling == beforeNode) {
+ return false;
+ } else if (sibling == candidate) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) {
+ UiElementNode parent = candidate.getUiParent();
+ if (parent != null) {
+ for (UiElementNode sibling : parent.getUiChildren()) {
+ if (sibling == afterNode) {
+ return true;
+ } else if (sibling == candidate) {
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ private UiViewElementNode nextViewNode(List<ViewInfo> children, int index) {
+ int size = children.size();
+ for (; index < size; index++) {
+ ViewInfo child = children.get(index);
+ if (child.getCookie() instanceof UiViewElementNode) {
+ return (UiViewElementNode) child.getCookie();
+ }
+ }
+
+ return null;
+ }
+
+ /** Search for a subtree with valid keys and add those subtrees */
+ private CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo,
+ int parentX, int parentY) {
+ // We don't include MergeCookies when searching down for the first non-null key,
+ // since this means we are in a "Show Included In" context, and the include tag itself
+ // (which the merge cookie is pointing to) is still in the including-document rather
+ // than the included document. Therefore, we only accept real UiViewElementNodes here,
+ // not MergeCookies.
+ if (viewInfo.getCookie() != null) {
+ CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY);
+ if (parent != null && subtree != null) {
+ parent.mChildren.add(subtree);
+ }
+ return subtree;
+ } else {
+ for (ViewInfo child : viewInfo.getChildren()) {
+ addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY
+ + viewInfo.getTop());
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java
new file mode 100644
index 000000000..263456984
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.NS_RESOURCES;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.common.api.INode;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.widgets.Composite;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The {@link ClipboardSupport} class manages the native clipboard, providing operations
+ * to copy, cut and paste view items, and can answer whether the clipboard contains
+ * a transferable we care about.
+ */
+public class ClipboardSupport {
+ private static final boolean DEBUG = false;
+
+ /** SWT clipboard instance. */
+ private Clipboard mClipboard;
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ClipboardSupport} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to provide clipboard support for.
+ * @param parent The parent widget in the SWT hierarchy of the canvas.
+ */
+ public ClipboardSupport(LayoutCanvas canvas, Composite parent) {
+ mCanvas = canvas;
+
+ mClipboard = new Clipboard(parent.getDisplay());
+ }
+
+ /**
+ * Frees up any resources held by the {@link ClipboardSupport}.
+ */
+ public void dispose() {
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+ }
+
+ /**
+ * Perform the "Copy" action, either from the Edit menu or from the context
+ * menu.
+ * <p/>
+ * This sanitizes the selection, so it must be a copy. It then inserts the
+ * selection both as text and as {@link SimpleElement}s in the clipboard.
+ * (If there is selected text in the error label, then the error is used
+ * as the text portion of the transferable.)
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void copySelectionToClipboard(List<SelectionItem> selection) {
+ SelectionManager.sanitize(selection);
+
+ // The error message area shares the copy action with the canvas. Invoking the
+ // copy action when there are errors visible *AND* the user has selected text there,
+ // should include the error message as the text transferable.
+ String message = null;
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ StyledText errorLabel = graphicalEditor.getErrorLabel();
+ if (errorLabel.getSelectionCount() > 0) {
+ message = errorLabel.getSelectionText();
+ }
+
+ if (selection.isEmpty()) {
+ if (message != null) {
+ mClipboard.setContents(
+ new Object[] { message },
+ new Transfer[] { TextTransfer.getInstance() }
+ );
+ }
+ return;
+ }
+
+ Object[] data = new Object[] {
+ SelectionItem.getAsElements(selection),
+ message != null ? message : SelectionItem.getAsText(mCanvas, selection)
+ };
+
+ Transfer[] types = new Transfer[] {
+ SimpleXmlTransfer.getInstance(),
+ TextTransfer.getInstance()
+ };
+
+ mClipboard.setContents(data, types);
+ }
+
+ /**
+ * Perform the "Cut" action, either from the Edit menu or from the context
+ * menu.
+ * <p/>
+ * This sanitizes the selection, so it must be a copy. It uses the
+ * {@link #copySelectionToClipboard(List)} method to copy the selection to
+ * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to
+ * delete the selection with a "Cut" verb for the title.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void cutSelectionToClipboard(List<SelectionItem> selection) {
+ copySelectionToClipboard(selection);
+ deleteSelection(mCanvas.getCutLabel(), selection);
+ }
+
+ /**
+ * Deletes the given selection.
+ *
+ * @param verb A translated verb for the action. Will be used for the
+ * undo/redo title. Typically this should be
+ * {@link Action#getText()} for either the cut or the delete
+ * actions in the canvas.
+ * @param selection The selection. Must not be null. Can be empty, in which
+ * case nothing happens. The selection list will be sanitized so
+ * the caller should pass in a copy.
+ */
+ public void deleteSelection(String verb, final List<SelectionItem> selection) {
+ SelectionManager.sanitize(selection);
+
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ // If all selected items have the same *kind* of parent, display that in the undo title.
+ String title = null;
+ for (SelectionItem cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null && vi.getParent() != null) {
+ CanvasViewInfo parent = vi.getParent();
+ assert parent != null;
+ if (title == null) {
+ title = parent.getName();
+ } else if (!title.equals(parent.getName())) {
+ // More than one kind of parent selected.
+ title = null;
+ break;
+ }
+ }
+ }
+
+ if (title != null) {
+ // Typically the name is an FQCN. Just get the last segment.
+ int pos = title.lastIndexOf('.');
+ if (pos > 0 && pos < title.length() - 1) {
+ title = title.substring(pos + 1);
+ }
+ }
+ boolean multiple = mCanvas.getSelectionManager().hasMultiSelection();
+ if (title == null) {
+ title = String.format(
+ multiple ? "%1$s elements" : "%1$s element",
+ verb);
+ } else {
+ title = String.format(
+ multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
+ verb, title);
+ }
+
+ // Implementation note: we don't clear the internal selection after removing
+ // the elements. An update XML model event should happen when the model gets released
+ // which will trigger a recompute of the layout, thus reloading the model thus
+ // resetting the selection.
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(title, new Runnable() {
+ @Override
+ public void run() {
+ // Segment the deleted nodes into clusters of siblings
+ Map<NodeProxy, List<INode>> clusters =
+ new HashMap<NodeProxy, List<INode>>();
+ for (SelectionItem cs : selection) {
+ NodeProxy node = cs.getNode();
+ if (node == null) {
+ continue;
+ }
+ INode parent = node.getParent();
+ if (parent != null) {
+ List<INode> children = clusters.get(parent);
+ if (children == null) {
+ children = new ArrayList<INode>();
+ clusters.put((NodeProxy) parent, children);
+ }
+ children.add(node);
+ }
+ }
+
+ // Notify parent views about children getting deleted
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) {
+ NodeProxy parent = entry.getKey();
+ List<INode> children = entry.getValue();
+ assert children != null && children.size() > 0;
+ rulesEngine.callOnRemovingChildren(parent, children);
+ parent.applyPendingChanges();
+ }
+
+ for (SelectionItem cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ // You can't delete the root element
+ if (vi != null && !vi.isRoot()) {
+ UiViewElementNode ui = vi.getUiViewNode();
+ if (ui != null) {
+ ui.deleteXmlNode();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Perform the "Paste" action, either from the Edit menu or from the context
+ * menu.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void pasteSelection(List<SelectionItem> selection) {
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ final SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
+
+ if (pasted == null || pasted.length == 0) {
+ return;
+ }
+
+ CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot();
+ if (lastRoot == null) {
+ // Pasting in an empty document. Only paste the first element.
+ pasteInEmptyDocument(pasted[0]);
+ return;
+ }
+
+ // Otherwise use the current selection, if any, as a guide where to paste
+ // using the first selected element only. If there's no selection use
+ // the root as the insertion point.
+ SelectionManager.sanitize(selection);
+ final CanvasViewInfo target;
+ if (selection.size() > 0) {
+ SelectionItem cs = selection.get(0);
+ target = cs.getViewInfo();
+ } else {
+ target = lastRoot;
+ }
+
+ final NodeProxy targetNode = mCanvas.getNodeFactory().create(target);
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Paste", new Runnable() {
+ @Override
+ public void run() {
+ RulesEngine engine = mCanvas.getRulesEngine();
+ NodeProxy node = engine.callOnPaste(targetNode, target.getViewObject(), pasted);
+ node.applyPendingChanges();
+ }
+ });
+ }
+
+ /**
+ * Paste a new root into an empty XML layout.
+ * <p/>
+ * In case of error (unknown FQCN, document not empty), silently do nothing.
+ * In case of success, the new element will have some default attributes set (xmlns:android,
+ * layout_width and height). The edit is wrapped in a proper undo.
+ * <p/>
+ * Implementation is similar to {@link #createDocumentRoot} except we also
+ * copy all the attributes and inner elements recursively.
+ */
+ private void pasteInEmptyDocument(final IDragElement pastedElement) {
+ String rootFqcn = pastedElement.getFqcn();
+
+ // Need a valid empty document to create the new root
+ final LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ final UiDocumentNode uiDoc = delegate.getUiRootNode();
+ if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
+ debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
+ return;
+ }
+
+ // Find the view descriptor matching our FQCN
+ final ViewElementDescriptor viewDesc = delegate.getFqcnViewDescriptor(rootFqcn);
+ if (viewDesc == null) {
+ // TODO this could happen if pasting a custom view not known in this project
+ debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
+ return;
+ }
+
+ // Get the last segment of the FQCN for the undo title
+ String title = rootFqcn;
+ int pos = title.lastIndexOf('.');
+ if (pos > 0 && pos < title.length() - 1) {
+ title = title.substring(pos + 1);
+ }
+ title = String.format("Paste root %1$s in document", title);
+
+ delegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() {
+ @Override
+ public void run() {
+ UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
+
+ // A root node requires the Android XMLNS
+ uiNew.setAttributeValue(ANDROID_NS_NAME, XMLNS_URI, NS_RESOURCES,
+ true /*override*/);
+
+ // Copy all the attributes from the pasted element
+ for (IDragAttribute attr : pastedElement.getAttributes()) {
+ uiNew.setAttributeValue(
+ attr.getName(),
+ attr.getUri(),
+ attr.getValue(),
+ true /*override*/);
+ }
+
+ // Adjust the attributes, adding the default layout_width/height
+ // only if they are not present (the original element should have
+ // them though.)
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
+
+ uiNew.createXmlNode();
+
+ // Now process all children
+ for (IDragElement childElement : pastedElement.getInnerElements()) {
+ addChild(uiNew, childElement);
+ }
+ }
+
+ private void addChild(UiElementNode uiParent, IDragElement childElement) {
+ String childFqcn = childElement.getFqcn();
+ final ViewElementDescriptor childDesc =
+ delegate.getFqcnViewDescriptor(childFqcn);
+ if (childDesc == null) {
+ // TODO this could happen if pasting a custom view
+ debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
+ return;
+ }
+
+ UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
+
+ // Copy all the attributes from the pasted element
+ for (IDragAttribute attr : childElement.getAttributes()) {
+ uiChild.setAttributeValue(
+ attr.getName(),
+ attr.getUri(),
+ attr.getValue(),
+ true /*override*/);
+ }
+
+ // Adjust the attributes, adding the default layout_width/height
+ // only if they are not present (the original element should have
+ // them though.)
+ DescriptorsUtils.setDefaultLayoutAttributes(
+ uiChild, false /*updateLayout*/);
+
+ uiChild.createXmlNode();
+
+ // Now process all grand children
+ for (IDragElement grandChildElement : childElement.getInnerElements()) {
+ addChild(uiChild, grandChildElement);
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns true if we have a a simple xml transfer data object on the
+ * clipboard.
+ *
+ * @return True if and only if the clipboard contains one of XML element
+ * objects.
+ */
+ public boolean hasSxtOnClipboard() {
+ // The paste operation is only available if we can paste our custom type.
+ // We do not currently support pasting random text (e.g. XML). Maybe later.
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ for (TransferData td : mClipboard.getAvailableTypes()) {
+ if (sxt.isSupportedType(td)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void debugPrintf(String message, Object... params) {
+ if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java
new file mode 100644
index 000000000..55930f6cd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Point;
+
+/**
+ * A {@link ControlPoint} is a coordinate in the canvas control which corresponds
+ * exactly to (0,0) at the top left of the canvas. It is unaffected by canvas
+ * zooming.
+ */
+public final class ControlPoint {
+ /** Containing canvas which the point is relative to. */
+ private final LayoutCanvas mCanvas;
+
+ /** The X coordinate of the mouse coordinate. */
+ public final int x;
+
+ /** The Y coordinate of the mouse coordinate. */
+ public final int y;
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event. The event
+ * must be from a {@link MouseListener} associated with the
+ * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+ * {@link MouseEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link MouseEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, MouseEvent event) {
+ // The mouse event coordinates should already be relative to the canvas
+ // widget.
+ assert event.widget == canvas : event.widget;
+ return new ControlPoint(canvas, event.x, event.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given menu detect event.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The menu detect event to construct the {@link ControlPoint} from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link MenuDetectEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, MenuDetectEvent event) {
+ // The menu detect events are always display-relative.
+ org.eclipse.swt.graphics.Point p = canvas.toControl(event.x, event.y);
+ return new ControlPoint(canvas, p.x, p.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event. The event
+ * must be from a {@link DragSourceListener} associated with the
+ * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+ * {@link DragSourceEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link DragSourceEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+ // The drag source event coordinates should already be relative to the
+ // canvas widget.
+ return new ControlPoint(canvas, event.x, event.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link DropTargetEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, DropTargetEvent event) {
+ // The drop target events are always relative to the display, so we must
+ // first convert them to be canvas relative.
+ org.eclipse.swt.graphics.Point p = canvas.toControl(event.x, event.y);
+ return new ControlPoint(canvas, p.x, p.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given x,y coordinates,
+ * which must be relative to the given {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param x The mouse event x coordinate relative to the canvas
+ * @param y The mouse event x coordinate relative to the canvas
+ * @return A {@link ControlPoint} which corresponds to the given
+ * coordinates.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, int x, int y) {
+ return new ControlPoint(canvas, x, y);
+ }
+
+ /**
+ * Constructs a new canvas control coordinate with the given X and Y
+ * coordinates. This is private; use one of the factory methods
+ * {@link #create(LayoutCanvas, MouseEvent)},
+ * {@link #create(LayoutCanvas, DragSourceEvent)} or
+ * {@link #create(LayoutCanvas, DropTargetEvent)} instead.
+ *
+ * @param canvas The canvas which contains this coordinate
+ * @param x The mouse x coordinate
+ * @param y The mouse y coordinate
+ */
+ private ControlPoint(LayoutCanvas canvas, int x, int y) {
+ mCanvas = canvas;
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Returns the equivalent {@link LayoutPoint} to this
+ * {@link ControlPoint}.
+ *
+ * @return The equivalent {@link LayoutPoint} to this
+ * {@link ControlPoint}.
+ */
+ public LayoutPoint toLayout() {
+ int lx = mCanvas.getHorizontalTransform().inverseTranslate(x);
+ int ly = mCanvas.getVerticalTransform().inverseTranslate(y);
+
+ return LayoutPoint.create(mCanvas, lx, ly);
+ }
+
+ @Override
+ public String toString() {
+ return "ControlPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ControlPoint other = (ControlPoint) obj;
+ if (x != other.x)
+ return false;
+ if (y != other.y)
+ return false;
+ if (mCanvas != other.mCanvas) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns this point as an SWT point in the display coordinate system
+ *
+ * @return this point as an SWT point in the display coordinate system
+ */
+ public Point toDisplayPoint() {
+ return mCanvas.toDisplay(x, y);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java
new file mode 100644
index 000000000..44cd0810f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceFolderType;
+import com.google.common.base.Charsets;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.PartInitException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/** Job which creates a new layout file for a given configuration */
+class CreateNewConfigJob extends Job {
+ private final GraphicalEditorPart mEditor;
+ private final IFile mFromFile;
+ private final FolderConfiguration mConfig;
+
+ CreateNewConfigJob(
+ @NonNull GraphicalEditorPart editor,
+ @NonNull IFile fromFile,
+ @NonNull FolderConfiguration config) {
+ super("Create Alternate Layout");
+ mEditor = editor;
+ mFromFile = fromFile;
+ mConfig = config;
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ // get the folder name
+ String folderName = mConfig.getFolderName(ResourceFolderType.LAYOUT);
+ try {
+ // look to see if it exists.
+ // get the res folder
+ IFolder res = (IFolder) mFromFile.getParent().getParent();
+
+ IFolder newParentFolder = res.getFolder(folderName);
+ AdtUtils.ensureExists(newParentFolder);
+ final IFile file = newParentFolder.getFile(mFromFile.getName());
+ if (file.exists()) {
+ String message = String.format("File 'res/%1$s/%2$s' already exists!",
+ folderName, mFromFile.getName());
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
+ }
+
+ // Read current document contents instead of from file: mFromFile.getContents()
+ String text = mEditor.getEditorDelegate().getEditor().getStructuredDocument().get();
+ ByteArrayInputStream input = new ByteArrayInputStream(text.getBytes(Charsets.UTF_8));
+ file.create(input, false, monitor);
+ input.close();
+
+ // Ensure that the project resources updates itself to notice the new configuration.
+ // In theory, this shouldn't be necessary, but we need to make sure the
+ // resource manager knows about this immediately such that the call below
+ // to find the best configuration takes the new folder into account.
+ ResourceManager resourceManager = ResourceManager.getInstance();
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IFolder folder = root.getFolder(newParentFolder.getFullPath());
+ resourceManager.getResourceFolder(folder);
+
+ // Switch to the new file
+ Display display = mEditor.getConfigurationChooser().getDisplay();
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ // The given old layout has been forked into a new layout
+ // for a given configuration. This means that the old layout
+ // is no longer a match for the configuration, which is
+ // probably what it is still showing. We have to modify
+ // its configuration to no longer be an impossible
+ // configuration.
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ chooser.onAlternateLayoutCreated();
+
+ // Finally open the new layout
+ try {
+ AdtPlugin.openFile(file, null, false);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ });
+ } catch (IOException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mFromFile.getName(), e2.getMessage());
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ message, e2);
+ } catch (CoreException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mFromFile.getName(), e2.getMessage());
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return e2.getStatus();
+ }
+
+ return Status.OK_STATUS;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java
new file mode 100644
index 000000000..1f97c8c54
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.CLASS_VIEWGROUP;
+import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+import org.eclipse.jdt.internal.core.ResolvedBinaryType;
+import org.eclipse.jdt.internal.core.ResolvedSourceType;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link CustomViewFinder} can look up the custom views and third party views
+ * available for a given project.
+ */
+@SuppressWarnings("restriction") // JDT model access for custom-view class lookup
+public class CustomViewFinder {
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link CustomViewFinder} for this project
+ */
+ private final static QualifiedName CUSTOM_VIEW_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "viewfinder"); //$NON-NLS-1$
+
+ /** Project that this view finder locates views for */
+ private final IProject mProject;
+
+ private final List<Listener> mListeners = new ArrayList<Listener>();
+
+ private List<String> mCustomViews;
+ private List<String> mThirdPartyViews;
+ private boolean mRefreshing;
+
+ /**
+ * Constructs an {@link CustomViewFinder} for the given project. Don't use this method;
+ * use the {@link #get} factory method instead.
+ *
+ * @param project project to create an {@link CustomViewFinder} for
+ */
+ private CustomViewFinder(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link CustomViewFinder} for the given project
+ *
+ * @param project the project the finder is associated with
+ * @return a {@CustomViewFinder} for the given project, never null
+ */
+ public static CustomViewFinder get(IProject project) {
+ CustomViewFinder finder = null;
+ try {
+ finder = (CustomViewFinder) project.getSessionProperty(CUSTOM_VIEW_FINDER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (finder == null) {
+ finder = new CustomViewFinder(project);
+ try {
+ project.setSessionProperty(CUSTOM_VIEW_FINDER, finder);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store CustomViewFinder");
+ }
+ }
+
+ return finder;
+ }
+
+ public void refresh() {
+ refresh(null /*listener*/, true /* sync */);
+ }
+
+ public void refresh(final Listener listener) {
+ refresh(listener, false /* sync */);
+ }
+
+ private void refresh(final Listener listener, boolean sync) {
+ // Add this listener to the list of listeners which should be notified when the
+ // search is done. (There could be more than one since multiple requests could
+ // arrive for a slow search since the search is run in a different thread).
+ if (listener != null) {
+ synchronized (this) {
+ mListeners.add(listener);
+ }
+ }
+ synchronized (this) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ if (mRefreshing) {
+ return;
+ }
+ mRefreshing = true;
+ }
+
+ FindViewsJob job = new FindViewsJob();
+ job.schedule();
+ if (sync) {
+ try {
+ job.join();
+ } catch (InterruptedException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ public Collection<String> getCustomViews() {
+ return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
+ }
+
+ public Collection<String> getThirdPartyViews() {
+ return mThirdPartyViews == null
+ ? null : Collections.unmodifiableCollection(mThirdPartyViews);
+ }
+
+ public Collection<String> getAllViews() {
+ // Not yet initialized: return null
+ if (mCustomViews == null) {
+ return null;
+ }
+ List<String> all = new ArrayList<String>(mCustomViews.size() + mThirdPartyViews.size());
+ all.addAll(mCustomViews);
+ all.addAll(mThirdPartyViews);
+ return all;
+ }
+
+ /**
+ * Returns a pair of view lists - the custom views and the 3rd-party views.
+ * This method performs no caching; it is the same as asking the custom view finder
+ * to refresh itself and then waiting for the answer and returning it.
+ *
+ * @param project the Android project
+ * @param layoutsOnly if true, only search for layouts
+ * @return a pair of lists, the first containing custom views and the second
+ * containing 3rd party views
+ */
+ public static Pair<List<String>,List<String>> findViews(
+ final IProject project, boolean layoutsOnly) {
+ CustomViewFinder finder = get(project);
+
+ return finder.findViews(layoutsOnly);
+ }
+
+ private Pair<List<String>,List<String>> findViews(final boolean layoutsOnly) {
+ final Set<String> customViews = new HashSet<String>();
+ final Set<String> thirdPartyViews = new HashSet<String>();
+
+ ProjectState state = Sdk.getProjectState(mProject);
+ final List<IProject> libraries = state != null
+ ? state.getFullLibraryProjects() : Collections.<IProject>emptyList();
+
+ SearchRequestor requestor = new SearchRequestor() {
+ @Override
+ public void acceptSearchMatch(SearchMatch match) throws CoreException {
+ // Ignore matches in comments
+ if (match.isInsideDocComment()) {
+ return;
+ }
+
+ Object element = match.getElement();
+ if (element instanceof ResolvedBinaryType) {
+ // Third party view
+ ResolvedBinaryType type = (ResolvedBinaryType) element;
+ IPackageFragment fragment = type.getPackageFragment();
+ IPath path = fragment.getPath();
+ String last = path.lastSegment();
+ // Filter out android.jar stuff
+ if (last.equals(FN_FRAMEWORK_LIBRARY)) {
+ return;
+ }
+ if (!isValidView(type, layoutsOnly)) {
+ return;
+ }
+
+ IProject matchProject = match.getResource().getProject();
+ if (mProject == matchProject || libraries.contains(matchProject)) {
+ String fqn = type.getFullyQualifiedName();
+ thirdPartyViews.add(fqn);
+ }
+ } else if (element instanceof ResolvedSourceType) {
+ // User custom view
+ IProject matchProject = match.getResource().getProject();
+ if (mProject == matchProject || libraries.contains(matchProject)) {
+ ResolvedSourceType type = (ResolvedSourceType) element;
+ if (!isValidView(type, layoutsOnly)) {
+ return;
+ }
+ String fqn = type.getFullyQualifiedName();
+ fqn = fqn.replace('$', '.');
+ customViews.add(fqn);
+ }
+ }
+ }
+ };
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
+ if (javaProject != null) {
+ String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW;
+ IType viewType = javaProject.findType(className);
+ if (viewType != null) {
+ IJavaSearchScope scope = SearchEngine.createHierarchyScope(viewType);
+ SearchParticipant[] participants = new SearchParticipant[] {
+ SearchEngine.getDefaultSearchParticipant()
+ };
+ int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
+
+ SearchPattern pattern = SearchPattern.createPattern("*",
+ IJavaSearchConstants.CLASS, IJavaSearchConstants.IMPLEMENTORS,
+ matchRule);
+ SearchEngine engine = new SearchEngine();
+ engine.search(pattern, participants, scope, requestor,
+ new NullProgressMonitor());
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+
+ List<String> custom = new ArrayList<String>(customViews);
+ List<String> thirdParty = new ArrayList<String>(thirdPartyViews);
+
+ if (!layoutsOnly) {
+ // Update our cached answers (unless we were filtered on only layouts)
+ mCustomViews = custom;
+ mThirdPartyViews = thirdParty;
+ }
+
+ return Pair.of(custom, thirdParty);
+ }
+
+ /**
+ * Determines whether the given member is a valid android.view.View to be added to the
+ * list of custom views or third party views. It checks that the view is public and
+ * not abstract for example.
+ */
+ private static boolean isValidView(IType type, boolean layoutsOnly)
+ throws JavaModelException {
+ // Skip anonymous classes
+ if (type.isAnonymous()) {
+ return false;
+ }
+ int flags = type.getFlags();
+ if (Flags.isAbstract(flags) || !Flags.isPublic(flags)) {
+ return false;
+ }
+
+ // TODO: if (layoutsOnly) perhaps try to filter out AdapterViews and other ViewGroups
+ // not willing to accept children via XML
+
+ // See if the class has one of the acceptable constructors
+ // needed for XML instantiation:
+ // View(Context context)
+ // View(Context context, AttributeSet attrs)
+ // View(Context context, AttributeSet attrs, int defStyle)
+ // We don't simply do three direct checks via type.getMethod() because the types
+ // are not resolved, so we don't know for each parameter if we will get the
+ // fully qualified or the unqualified class names.
+ // Instead, iterate over the methods and look for a match.
+ String typeName = type.getElementName();
+ for (IMethod method : type.getMethods()) {
+ // Only care about constructors
+ if (!method.getElementName().equals(typeName)) {
+ continue;
+ }
+
+ String[] parameterTypes = method.getParameterTypes();
+ if (parameterTypes == null || parameterTypes.length < 1 || parameterTypes.length > 3) {
+ continue;
+ }
+
+ String first = parameterTypes[0];
+ // Look for the parameter type signatures -- produced by
+ // JDT's Signature.createTypeSignature("Context", false /*isResolved*/);.
+ // This is not a typo; they were copy/pasted from the actual parameter names
+ // observed in the debugger examining these data structures.
+ if (first.equals("QContext;") //$NON-NLS-1$
+ || first.equals("Qandroid.content.Context;")) { //$NON-NLS-1$
+ if (parameterTypes.length == 1) {
+ return true;
+ }
+ String second = parameterTypes[1];
+ if (second.equals("QAttributeSet;") //$NON-NLS-1$
+ || second.equals("Qandroid.util.AttributeSet;")) { //$NON-NLS-1$
+ if (parameterTypes.length == 2) {
+ return true;
+ }
+ String third = parameterTypes[2];
+ if (third.equals("I")) { //$NON-NLS-1$
+ if (parameterTypes.length == 3) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Interface implemented by clients of the {@link CustomViewFinder} to be notified
+ * when a custom view search has completed. Will always be called on the SWT event
+ * dispatch thread.
+ */
+ public interface Listener {
+ void viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews);
+ }
+
+ /**
+ * Job for performing class search off the UI thread. This is marked as a system job
+ * so that it won't show up in the progress monitor etc.
+ */
+ private class FindViewsJob extends Job {
+ FindViewsJob() {
+ super("Find Custom Views");
+ setSystem(true);
+ }
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ Pair<List<String>, List<String>> views = findViews(false);
+ mCustomViews = views.getFirst();
+ mThirdPartyViews = views.getSecond();
+
+ // Notify listeners on SWT's UI thread
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ Collection<String> customViews =
+ Collections.unmodifiableCollection(mCustomViews);
+ Collection<String> thirdPartyViews =
+ Collections.unmodifiableCollection(mThirdPartyViews);
+ synchronized (this) {
+ for (Listener l : mListeners) {
+ l.viewsUpdated(customViews, thirdPartyViews);
+ }
+ mListeners.clear();
+ mRefreshing = false;
+ }
+ }
+ });
+ return Status.OK_STATUS;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java
new file mode 100644
index 000000000..7a41b5b15
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuCreator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.swt.events.HelpListener;
+import org.eclipse.swt.widgets.Event;
+
+/**
+ * Implementation of {@link IAction} which delegates to a different
+ * {@link IAction} which allows a subclass to wrap and customize some of the
+ * behavior of a different action
+ */
+public class DelegatingAction implements IAction {
+ private final IAction mAction;
+
+ /**
+ * Construct a new delegate of the given action
+ *
+ * @param action the action to be delegated
+ */
+ public DelegatingAction(@NonNull IAction action) {
+ mAction = action;
+ }
+
+ @Override
+ public void addPropertyChangeListener(IPropertyChangeListener listener) {
+ mAction.addPropertyChangeListener(listener);
+ }
+
+ @Override
+ public int getAccelerator() {
+ return mAction.getAccelerator();
+ }
+
+ @Override
+ public String getActionDefinitionId() {
+ return mAction.getActionDefinitionId();
+ }
+
+ @Override
+ public String getDescription() {
+ return mAction.getDescription();
+ }
+
+ @Override
+ public ImageDescriptor getDisabledImageDescriptor() {
+ return mAction.getDisabledImageDescriptor();
+ }
+
+ @Override
+ public HelpListener getHelpListener() {
+ return mAction.getHelpListener();
+ }
+
+ @Override
+ public ImageDescriptor getHoverImageDescriptor() {
+ return mAction.getHoverImageDescriptor();
+ }
+
+ @Override
+ public String getId() {
+ return mAction.getId();
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return mAction.getImageDescriptor();
+ }
+
+ @Override
+ public IMenuCreator getMenuCreator() {
+ return mAction.getMenuCreator();
+ }
+
+ @Override
+ public int getStyle() {
+ return mAction.getStyle();
+ }
+
+ @Override
+ public String getText() {
+ return mAction.getText();
+ }
+
+ @Override
+ public String getToolTipText() {
+ return mAction.getToolTipText();
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mAction.isChecked();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mAction.isEnabled();
+ }
+
+ @Override
+ public boolean isHandled() {
+ return mAction.isHandled();
+ }
+
+ @Override
+ public void removePropertyChangeListener(IPropertyChangeListener listener) {
+ mAction.removePropertyChangeListener(listener);
+ }
+
+ @Override
+ public void run() {
+ mAction.run();
+ }
+
+ @Override
+ public void runWithEvent(Event event) {
+ mAction.runWithEvent(event);
+ }
+
+ @Override
+ public void setActionDefinitionId(String id) {
+ mAction.setActionDefinitionId(id);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mAction.setChecked(checked);
+ }
+
+ @Override
+ public void setDescription(String text) {
+ mAction.setDescription(text);
+ }
+
+ @Override
+ public void setDisabledImageDescriptor(ImageDescriptor newImage) {
+ mAction.setDisabledImageDescriptor(newImage);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mAction.setEnabled(enabled);
+ }
+
+ @Override
+ public void setHelpListener(HelpListener listener) {
+ mAction.setHelpListener(listener);
+ }
+
+ @Override
+ public void setHoverImageDescriptor(ImageDescriptor newImage) {
+ mAction.setHoverImageDescriptor(newImage);
+ }
+
+ @Override
+ public void setId(String id) {
+ mAction.setId(id);
+ }
+
+ @Override
+ public void setImageDescriptor(ImageDescriptor newImage) {
+ mAction.setImageDescriptor(newImage);
+ }
+
+ @Override
+ public void setMenuCreator(IMenuCreator creator) {
+ mAction.setMenuCreator(creator);
+ }
+
+ @Override
+ public void setText(String text) {
+ mAction.setText(text);
+ }
+
+ @Override
+ public void setToolTipText(String text) {
+ mAction.setToolTipText(text);
+ }
+
+ @Override
+ public void setAccelerator(int keycode) {
+ mAction.setAccelerator(keycode);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
new file mode 100644
index 000000000..145036bf3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
@@ -0,0 +1,915 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.TOOLS_URI;
+import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Various utility methods for manipulating DOM nodes.
+ */
+@SuppressWarnings("restriction") // No replacement for restricted XML model yet
+public class DomUtilities {
+ /**
+ * Finds the nearest common parent of the two given nodes (which could be one of the
+ * two nodes as well)
+ *
+ * @param node1 the first node to test
+ * @param node2 the second node to test
+ * @return the nearest common parent of the two given nodes
+ */
+ @Nullable
+ public static Node getCommonAncestor(@NonNull Node node1, @NonNull Node node2) {
+ while (node2 != null) {
+ Node current = node1;
+ while (current != null && current != node2) {
+ current = current.getParentNode();
+ }
+ if (current == node2) {
+ return current;
+ }
+ node2 = node2.getParentNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns all elements below the given node (which can be a document,
+ * element, etc). This will include the node itself, if it is an element.
+ *
+ * @param node the node to search from
+ * @return all elements in the subtree formed by the node parameter
+ */
+ @NonNull
+ public static List<Element> getAllElements(@NonNull Node node) {
+ List<Element> elements = new ArrayList<Element>(64);
+ addElements(node, elements);
+ return elements;
+ }
+
+ private static void addElements(@NonNull Node node, @NonNull List<Element> elements) {
+ if (node instanceof Element) {
+ elements.add((Element) node);
+ }
+
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ addElements(childNodes.item(i), elements);
+ }
+ }
+
+ /**
+ * Returns the depth of the given node (with the document node having depth 0,
+ * and the document element having depth 1)
+ *
+ * @param node the node to test
+ * @return the depth in the document
+ */
+ public static int getDepth(@NonNull Node node) {
+ int depth = -1;
+ while (node != null) {
+ depth++;
+ node = node.getParentNode();
+ }
+
+ return depth;
+ }
+
+ /**
+ * Returns true if the given node has one or more element children
+ *
+ * @param node the node to test for element children
+ * @return true if the node has one or more element children
+ */
+ public static boolean hasElementChildren(@NonNull Node node) {
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the DOM document for the given file
+ *
+ * @param file the XML file
+ * @return the document, or null if not found or not parsed properly (no
+ * errors are generated/thrown)
+ */
+ @Nullable
+ public static Document getDocument(@NonNull IFile file) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(file);
+ if (model == null) {
+ model = modelManager.getModelForRead(file);
+ }
+ if (model != null) {
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+ try {
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the DOM document for the given editor
+ *
+ * @param editor the XML editor
+ * @return the document, or null if not found or not parsed properly (no
+ * errors are generated/thrown)
+ */
+ @Nullable
+ public static Document getDocument(@NonNull AndroidXmlEditor editor) {
+ IStructuredModel model = editor.getModelForRead();
+ try {
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns the XML DOM node corresponding to the given offset of the given
+ * document.
+ *
+ * @param document The document to look in
+ * @param offset The offset to look up the node for
+ * @return The node containing the offset, or null
+ */
+ @Nullable
+ public static Node getNode(@NonNull IDocument document, int offset) {
+ Node node = null;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(document);
+ if (model != null) {
+ try {
+ for (; offset >= 0 && node == null; --offset) {
+ node = (Node) model.getIndexedRegion(offset);
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return node;
+ }
+
+ /**
+ * Returns the editing context at the given offset, as a pair of parent node and child
+ * node. This is not the same as just calling {@link DomUtilities#getNode} and taking
+ * its parent node, because special care has to be taken to return content element
+ * positions.
+ * <p>
+ * For example, for the XML {@code <foo>^</foo>}, if the caret ^ is inside the foo
+ * element, between the opening and closing tags, then the foo element is the parent,
+ * and the child is null which represents a potential text node.
+ * <p>
+ * If the node is inside an element tag definition (between the opening and closing
+ * bracket) then the child node will be the element and whatever parent (element or
+ * document) will be its parent.
+ * <p>
+ * If the node is in a text node, then the text node will be the child and its parent
+ * element or document node its parent.
+ * <p>
+ * Finally, if the caret is on a boundary of a text node, then the text node will be
+ * considered the child, regardless of whether it is on the left or right of the
+ * caret. For example, in the XML {@code <foo>^ </foo>} and in the XML
+ * {@code <foo> ^</foo>}, in both cases the text node is preferred over the element.
+ *
+ * @param document the document to search in
+ * @param offset the offset to look up
+ * @return a pair of parent and child elements, where either the parent or the child
+ * but not both can be null, and if non null the child.getParentNode() should
+ * return the parent. Note that the method can also return null if no
+ * document or model could be obtained or if the offset is invalid.
+ */
+ @Nullable
+ public static Pair<Node, Node> getNodeContext(@NonNull IDocument document, int offset) {
+ Node node = null;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(document);
+ if (model != null) {
+ try {
+ for (; offset >= 0 && node == null; --offset) {
+ IndexedRegion indexedRegion = model.getIndexedRegion(offset);
+ if (indexedRegion != null) {
+ node = (Node) indexedRegion;
+
+ if (node.getNodeType() == Node.TEXT_NODE) {
+ return Pair.of(node.getParentNode(), node);
+ }
+
+ // Look at the structured document to see if
+ // we have the special case where the caret is pointing at
+ // a -potential- text node, e.g. <foo>^</foo>
+ IStructuredDocument doc = model.getStructuredDocument();
+ IStructuredDocumentRegion region =
+ doc.getRegionAtCharacterOffset(offset);
+
+ ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
+ // Try to return the text node if it's on the left
+ // of this element node, such that replace strings etc
+ // can be computed.
+ Node lastChild = node.getLastChild();
+ if (lastChild != null) {
+ IndexedRegion previousRegion = (IndexedRegion) lastChild;
+ if (previousRegion.getEndOffset() == offset) {
+ return Pair.of(node, lastChild);
+ }
+ }
+ return Pair.of(node, null);
+ }
+
+ return Pair.of(node.getParentNode(), node);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return null;
+ }
+
+ /**
+ * Like {@link #getNode(IDocument, int)}, but has a bias parameter which lets you
+ * indicate whether you want the search to look forwards or backwards.
+ * This is vital when trying to compute a node range. Consider the following
+ * XML fragment:
+ * {@code
+ * <a/><b/>[<c/><d/><e/>]<f/><g/>
+ * }
+ * Suppose we want to locate the nodes in the range indicated by the brackets above.
+ * If we want to search for the node corresponding to the start position, should
+ * we pick the node on its left or the node on its right? Similarly for the end
+ * position. Clearly, we'll need to bias the search towards the right when looking
+ * for the start position, and towards the left when looking for the end position.
+ * The following method lets us do just that. When passed an offset which sits
+ * on the edge of the computed node, it will pick the neighbor based on whether
+ * "forward" is true or false, where forward means searching towards the right
+ * and not forward is obviously towards the left.
+ * @param document the document to search in
+ * @param offset the offset to search for
+ * @param forward if true, search forwards, otherwise search backwards when on node boundaries
+ * @return the node which surrounds the given offset, or the node adjacent to the offset
+ * where the side depends on the forward parameter
+ */
+ @Nullable
+ public static Node getNode(@NonNull IDocument document, int offset, boolean forward) {
+ Node node = getNode(document, offset);
+
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+
+ if (!forward && offset <= region.getStartOffset()) {
+ Node left = node.getPreviousSibling();
+ if (left == null) {
+ left = node.getParentNode();
+ }
+
+ node = left;
+ } else if (forward && offset >= region.getEndOffset()) {
+ Node right = node.getNextSibling();
+ if (right == null) {
+ right = node.getParentNode();
+ }
+ node = right;
+ }
+ }
+
+ return node;
+ }
+
+ /**
+ * Returns a range of elements for the given caret range. Note that the two elements
+ * may not be at the same level so callers may want to perform additional input
+ * filtering.
+ *
+ * @param document the document to search in
+ * @param beginOffset the beginning offset of the range
+ * @param endOffset the ending offset of the range
+ * @return a pair of begin+end elements, or null
+ */
+ @Nullable
+ public static Pair<Element, Element> getElementRange(@NonNull IDocument document,
+ int beginOffset, int endOffset) {
+ Element beginElement = null;
+ Element endElement = null;
+ Node beginNode = getNode(document, beginOffset, true);
+ Node endNode = beginNode;
+ if (endOffset > beginOffset) {
+ endNode = getNode(document, endOffset, false);
+ }
+
+ if (beginNode == null || endNode == null) {
+ return null;
+ }
+
+ // Adjust offsets if you're pointing at text
+ if (beginNode.getNodeType() != Node.ELEMENT_NODE) {
+ // <foo> <bar1/> | <bar2/> </foo> => should pick <bar2/>
+ beginElement = getNextElement(beginNode);
+ if (beginElement == null) {
+ // Might be inside the end of a parent, e.g.
+ // <foo> <bar/> | </foo> => should pick <bar/>
+ beginElement = getPreviousElement(beginNode);
+ if (beginElement == null) {
+ // We must be inside an empty element,
+ // <foo> | </foo>
+ // In that case just pick the parent.
+ beginElement = getParentElement(beginNode);
+ }
+ }
+ } else {
+ beginElement = (Element) beginNode;
+ }
+
+ if (endNode.getNodeType() != Node.ELEMENT_NODE) {
+ // In the following, | marks the caret position:
+ // <foo> <bar1/> | <bar2/> </foo> => should pick <bar1/>
+ endElement = getPreviousElement(endNode);
+ if (endElement == null) {
+ // Might be inside the beginning of a parent, e.g.
+ // <foo> | <bar/></foo> => should pick <bar/>
+ endElement = getNextElement(endNode);
+ if (endElement == null) {
+ // We must be inside an empty element,
+ // <foo> | </foo>
+ // In that case just pick the parent.
+ endElement = getParentElement(endNode);
+ }
+ }
+ } else {
+ endElement = (Element) endNode;
+ }
+
+ if (beginElement != null && endElement != null) {
+ return Pair.of(beginElement, endElement);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the next sibling element of the node, or null if there is no such element
+ *
+ * @param node the starting node
+ * @return the next sibling element, or null
+ */
+ @Nullable
+ public static Element getNextElement(@NonNull Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getNextSibling();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Returns the previous sibling element of the node, or null if there is no such element
+ *
+ * @param node the starting node
+ * @return the previous sibling element, or null
+ */
+ @Nullable
+ public static Element getPreviousElement(@NonNull Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getPreviousSibling();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Returns the closest ancestor element, or null if none
+ *
+ * @param node the starting node
+ * @return the closest parent element, or null
+ */
+ @Nullable
+ public static Element getParentElement(@NonNull Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getParentNode();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /** Utility used by {@link #getFreeWidgetId(Element)} */
+ private static void addLowercaseIds(@NonNull Element root, @NonNull Set<String> seen) {
+ if (root.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
+ String id = root.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ // See getFreeWidgetId for details on locale
+ seen.add(id.substring(NEW_ID_PREFIX.length()).toLowerCase(Locale.US));
+ } else if (id.startsWith(ID_PREFIX)) {
+ seen.add(id.substring(ID_PREFIX.length()).toLowerCase(Locale.US));
+ } else {
+ seen.add(id.toLowerCase(Locale.US));
+ }
+ }
+ }
+
+ /**
+ * Returns a suitable new widget id (not including the {@code @id/} prefix) for the
+ * given element, which is guaranteed to be unique in this document
+ *
+ * @param element the element to compute a new widget id for
+ * @param reserved an optional set of extra, "reserved" set of ids that should be
+ * considered taken
+ * @param prefix an optional prefix to use for the generated name, or null to get a
+ * default (which is currently the tag name)
+ * @return a unique id, never null, which does not include the {@code @id/} prefix
+ * @see DescriptorsUtils#getFreeWidgetId
+ */
+ public static String getFreeWidgetId(
+ @NonNull Element element,
+ @Nullable Set<String> reserved,
+ @Nullable String prefix) {
+ Set<String> ids = new HashSet<String>();
+ if (reserved != null) {
+ for (String id : reserved) {
+ // Note that we perform locale-independent lowercase checks; in "Image" we
+ // want the lowercase version to be "image", not "?mage" where ? is
+ // the char LATIN SMALL LETTER DOTLESS I.
+
+ ids.add(id.toLowerCase(Locale.US));
+ }
+ }
+ addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids);
+
+ if (prefix == null) {
+ prefix = DescriptorsUtils.getBasename(element.getTagName());
+ }
+ String generated;
+ int num = 1;
+ do {
+ generated = String.format("%1$s%2$d", prefix, num++); //$NON-NLS-1$
+ } while (ids.contains(generated.toLowerCase(Locale.US)));
+
+ return generated;
+ }
+
+ /**
+ * Returns the element children of the given element
+ *
+ * @param element the parent element
+ * @return a list of child elements, possibly empty but never null
+ */
+ @NonNull
+ public static List<Element> getChildren(@NonNull Element element) {
+ // Convenience to avoid lots of ugly DOM access casting
+ NodeList children = element.getChildNodes();
+ // An iterator would have been more natural (to directly drive the child list
+ // iteration) but iterators can't be used in enhanced for loops...
+ List<Element> result = new ArrayList<Element>(children.getLength());
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ result.add(child);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns true iff the given elements are contiguous siblings
+ *
+ * @param elements the elements to be tested
+ * @return true if the elements are contiguous siblings with no gaps
+ */
+ public static boolean isContiguous(@NonNull List<Element> elements) {
+ if (elements.size() > 1) {
+ // All elements must be siblings (e.g. same parent)
+ Node parent = elements.get(0).getParentNode();
+ if (!(parent instanceof Element)) {
+ return false;
+ }
+ for (Element node : elements) {
+ if (parent != node.getParentNode()) {
+ return false;
+ }
+ }
+
+ // Ensure that the siblings are contiguous; no gaps.
+ // If we've selected all the children of the parent then we don't need
+ // to look.
+ List<Element> siblings = DomUtilities.getChildren((Element) parent);
+ if (siblings.size() != elements.size()) {
+ Set<Element> nodeSet = new HashSet<Element>(elements);
+ boolean inRange = false;
+ int remaining = elements.size();
+ for (Element node : siblings) {
+ boolean in = nodeSet.contains(node);
+ if (in) {
+ remaining--;
+ if (remaining == 0) {
+ break;
+ }
+ inRange = true;
+ } else if (inRange) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determines whether two element trees are equivalent. Two element trees are
+ * equivalent if they represent the same DOM structure (elements, attributes, and
+ * children in order). This is almost the same as simply checking whether the String
+ * representations of the two nodes are identical, but this allows for minor
+ * variations that are not semantically significant, such as variations in formatting
+ * or ordering of the element attribute declarations, and the text children are
+ * ignored (this is such that in for example layout where content is only used for
+ * indentation the indentation differences are ignored). Null trees are never equal.
+ *
+ * @param element1 the first element to compare
+ * @param element2 the second element to compare
+ * @return true if the two element hierarchies are logically equal
+ */
+ public static boolean isEquivalent(@Nullable Element element1, @Nullable Element element2) {
+ if (element1 == null || element2 == null) {
+ return false;
+ }
+
+ if (!element1.getTagName().equals(element2.getTagName())) {
+ return false;
+ }
+
+ // Check attribute map
+ NamedNodeMap attributes1 = element1.getAttributes();
+ NamedNodeMap attributes2 = element2.getAttributes();
+
+ List<Attr> attributeNodes1 = new ArrayList<Attr>();
+ for (int i = 0, n = attributes1.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes1.item(i);
+ // Ignore tools uri namespace attributes for equivalency test
+ if (TOOLS_URI.equals(attribute.getNamespaceURI())) {
+ continue;
+ }
+ attributeNodes1.add(attribute);
+ }
+ List<Attr> attributeNodes2 = new ArrayList<Attr>();
+ for (int i = 0, n = attributes2.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes2.item(i);
+ // Ignore tools uri namespace attributes for equivalency test
+ if (TOOLS_URI.equals(attribute.getNamespaceURI())) {
+ continue;
+ }
+ attributeNodes2.add(attribute);
+ }
+
+ if (attributeNodes1.size() != attributeNodes2.size()) {
+ return false;
+ }
+
+ if (attributes1.getLength() > 0) {
+ Collections.sort(attributeNodes1, ATTRIBUTE_COMPARATOR);
+ Collections.sort(attributeNodes2, ATTRIBUTE_COMPARATOR);
+ for (int i = 0; i < attributeNodes1.size(); i++) {
+ Attr attr1 = attributeNodes1.get(i);
+ Attr attr2 = attributeNodes2.get(i);
+ if (attr1.getLocalName() == null || attr2.getLocalName() == null) {
+ if (!attr1.getName().equals(attr2.getName())) {
+ return false;
+ }
+ } else if (!attr1.getLocalName().equals(attr2.getLocalName())) {
+ return false;
+ }
+ if (!attr1.getValue().equals(attr2.getValue())) {
+ return false;
+ }
+ if (attr1.getNamespaceURI() == null) {
+ if (attr2.getNamespaceURI() != null) {
+ return false;
+ }
+ } else if (attr2.getNamespaceURI() == null) {
+ return false;
+ } else if (!attr1.getNamespaceURI().equals(attr2.getNamespaceURI())) {
+ return false;
+ }
+ }
+ }
+
+ NodeList children1 = element1.getChildNodes();
+ NodeList children2 = element2.getChildNodes();
+ int nextIndex1 = 0;
+ int nextIndex2 = 0;
+ while (true) {
+ while (nextIndex1 < children1.getLength() &&
+ children1.item(nextIndex1).getNodeType() != Node.ELEMENT_NODE) {
+ nextIndex1++;
+ }
+
+ while (nextIndex2 < children2.getLength() &&
+ children2.item(nextIndex2).getNodeType() != Node.ELEMENT_NODE) {
+ nextIndex2++;
+ }
+
+ Element nextElement1 = (Element) (nextIndex1 < children1.getLength()
+ ? children1.item(nextIndex1) : null);
+ Element nextElement2 = (Element) (nextIndex2 < children2.getLength()
+ ? children2.item(nextIndex2) : null);
+ if (nextElement1 == null) {
+ return nextElement2 == null;
+ } else if (nextElement2 == null) {
+ return false;
+ } else if (!isEquivalent(nextElement1, nextElement2)) {
+ return false;
+ }
+ nextIndex1++;
+ nextIndex2++;
+ }
+ }
+
+ /**
+ * Finds the corresponding element in a document to a given element in another
+ * document. Note that this does <b>not</b> do any kind of equivalence check
+ * (see {@link #isEquivalent(Element, Element)}), and currently the search
+ * is only by id; there is no structural search.
+ *
+ * @param element the element to find an equivalent for
+ * @param document the document to search for an equivalent element in
+ * @return an equivalent element, or null
+ */
+ @Nullable
+ public static Element findCorresponding(@NonNull Element element, @NonNull Document document) {
+ // Make sure the method is called correctly -- the element is for a different
+ // document than the one we are searching
+ assert element.getOwnerDocument() != document;
+
+ // First search by id. This allows us to find the corresponding
+ String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ }
+
+ return findCorresponding(document.getDocumentElement(), id);
+ }
+
+ // TODO: Search by structure - look in the document and
+ // find a corresponding element in the same location in the structure,
+ // e.g. 4th child of root, 3rd child, 6th child, then pick node with tag "foo".
+
+ return null;
+ }
+
+ /** Helper method for {@link #findCorresponding(Element, Document)} */
+ @Nullable
+ private static Element findCorresponding(@NonNull Element element, @NonNull String targetId) {
+ String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id != null) { // Work around DOM bug
+ if (id.equals(targetId)) {
+ return element;
+ } else if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ if (id.equals(targetId)) {
+ return element;
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ Element match = findCorresponding(child, targetId);
+ if (match != null) {
+ return match;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the given XML string as a DOM document, using Eclipse's structured
+ * XML model (which for example allows us to distinguish empty elements
+ * (<foo/>) from elements with no children (<foo></foo>).
+ *
+ * @param xml the XML content to be parsed (must be well formed)
+ * @return the DOM document, or null
+ */
+ @Nullable
+ public static Document parseStructuredDocument(@NonNull String xml) {
+ IStructuredModel model = createStructuredModel(xml);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the given XML string and builds an Eclipse structured model for it.
+ *
+ * @param xml the XML content to be parsed (must be well formed)
+ * @return the structured model
+ */
+ @Nullable
+ public static IStructuredModel createStructuredModel(@NonNull String xml) {
+ IStructuredModel model = createEmptyModel();
+ IStructuredDocument document = model.getStructuredDocument();
+ model.aboutToChangeModel();
+ document.set(xml);
+ model.changedModel();
+
+ return model;
+ }
+
+ /**
+ * Creates an empty Eclipse XML model
+ *
+ * @return a new Eclipse XML model
+ */
+ @NonNull
+ public static IStructuredModel createEmptyModel() {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ return modelManager.createUnManagedStructuredModelFor(ContentTypeID_XML);
+ }
+
+ /**
+ * Creates an empty Eclipse XML document
+ *
+ * @return an empty Eclipse XML document
+ */
+ @Nullable
+ public static Document createEmptyDocument() {
+ IStructuredModel model = createEmptyModel();
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an empty non-Eclipse XML document.
+ * This is used when you need to use XML operations not supported by
+ * the Eclipse XML model (such as serialization).
+ * <p>
+ * The new document will not validate, will ignore comments, and will
+ * support namespace.
+ *
+ * @return the new document
+ */
+ @Nullable
+ public static Document createEmptyPlainDocument() {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ factory.setIgnoringComments(true);
+ DocumentBuilder builder;
+ try {
+ builder = factory.newDocumentBuilder();
+ return builder.newDocument();
+ } catch (ParserConfigurationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the given XML string as a DOM document, using the JDK parser.
+ * The parser does not validate, and is namespace aware.
+ *
+ * @param xml the XML content to be parsed (must be well formed)
+ * @param logParserErrors if true, log parser errors to the log, otherwise
+ * silently return null
+ * @return the DOM document, or null
+ */
+ @Nullable
+ public static Document parseDocument(@NonNull String xml, boolean logParserErrors) {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ InputSource is = new InputSource(new StringReader(xml));
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ try {
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ return builder.parse(is);
+ } catch (Exception e) {
+ if (logParserErrors) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return null;
+ }
+
+ /** Can be used to sort attributes by name */
+ private static final Comparator<Attr> ATTRIBUTE_COMPARATOR = new Comparator<Attr>() {
+ @Override
+ public int compare(Attr a1, Attr a2) {
+ return a1.getName().compareTo(a2.getName());
+ }
+ };
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java
new file mode 100644
index 000000000..bb3be7f68
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+
+/**
+ * A {@link DropGesture} is a {@link Gesture} which deals with drag and drop, so
+ * it has additional hooks for indicating whether the current position is
+ * "valid", and in general gets access to the system drag and drop data
+ * structures. See the {@link Gesture} documentation for more details on whether
+ * you should choose a plain {@link Gesture} or a {@link DropGesture}.
+ */
+public abstract class DropGesture extends Gesture {
+ /**
+ * The cursor has entered the drop target boundaries.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragEnter(DropTargetEvent)
+ */
+ public void dragEnter(DropTargetEvent event) {
+ }
+
+ /**
+ * The cursor is moving over the drop target.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragOver(DropTargetEvent)
+ */
+ public void dragOver(DropTargetEvent event) {
+ }
+
+ /**
+ * The operation being performed has changed (usually due to the user
+ * changing the selected modifier key(s) while dragging).
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragOperationChanged(DropTargetEvent)
+ */
+ public void dragOperationChanged(DropTargetEvent event) {
+ }
+
+ /**
+ * The cursor has left the drop target boundaries OR the drop has been
+ * canceled OR the data is about to be dropped.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragLeave(DropTargetEvent)
+ */
+ public void dragLeave(DropTargetEvent event) {
+ }
+
+ /**
+ * The drop is about to be performed. The drop target is given a last chance
+ * to change the nature of the drop.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dropAccept(DropTargetEvent)
+ */
+ public void dropAccept(DropTargetEvent event) {
+ }
+
+ /**
+ * The data is being dropped. The data field contains java format of the
+ * data being dropped.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#drop(DropTargetEvent)
+ */
+ public void drop(final DropTargetEvent event) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
new file mode 100644
index 000000000..fc7127278
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.FQCN_IMAGE_VIEW;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.FQCN_TEXT_VIEW;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.LIST_VIEW;
+import static com.android.SdkConstants.SPINNER;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
+import com.android.ide.common.api.RuleAction.NestedAction;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractStyleAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UnwrapAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UseCompoundDrawableAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.ContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Menu;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class that is responsible for adding and managing the dynamic menu items
+ * contributed by the {@link IViewRule} instances, based on the current selection
+ * on the {@link LayoutCanvas}.
+ * <p/>
+ * This class is tied to a specific {@link LayoutCanvas} instance and a root {@link MenuManager}.
+ * <p/>
+ * Two instances of this are used: one created by {@link LayoutCanvas} and the other one
+ * created by {@link OutlinePage}. Different root {@link MenuManager}s are populated, however
+ * they are both linked to the current selection state of the {@link LayoutCanvas}.
+ */
+class DynamicContextMenu {
+ public static String DEFAULT_ACTION_SHORTCUT = "F2"; //$NON-NLS-1$
+ public static int DEFAULT_ACTION_KEY = SWT.F2;
+
+ /** The XML layout editor that contains the canvas that uses this menu. */
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ /** The layout canvas that displays this context menu. */
+ private final LayoutCanvas mCanvas;
+
+ /** The root menu manager of the context menu. */
+ private final MenuManager mMenuManager;
+
+ /**
+ * Creates a new helper responsible for adding and managing the dynamic menu items
+ * contributed by the {@link IViewRule} instances, based on the current selection
+ * on the {@link LayoutCanvas}.
+ * @param editorDelegate the editor owning the menu
+ * @param canvas The {@link LayoutCanvas} providing the selection, the node factory and
+ * the rules engine.
+ * @param rootMenu The root of the context menu displayed. In practice this may be the
+ * context menu manager of the {@link LayoutCanvas} or the one from {@link OutlinePage}.
+ */
+ public DynamicContextMenu(
+ LayoutEditorDelegate editorDelegate,
+ LayoutCanvas canvas,
+ MenuManager rootMenu) {
+ mEditorDelegate = editorDelegate;
+ mCanvas = canvas;
+ mMenuManager = rootMenu;
+
+ setupDynamicMenuActions();
+ }
+
+ /**
+ * Setups the menu manager to receive dynamic menu contributions from the {@link IViewRule}s
+ * when it's about to be shown.
+ */
+ private void setupDynamicMenuActions() {
+ // Remember how many static actions we have. Then each time the menu is
+ // shown, find dynamic contributions based on the current selection and insert
+ // them at the beginning of the menu.
+ final int numStaticActions = mMenuManager.getSize();
+ mMenuManager.addMenuListener(new IMenuListener() {
+ @Override
+ public void menuAboutToShow(IMenuManager manager) {
+
+ // Remove any previous dynamic contributions to keep only the
+ // default static items.
+ int n = mMenuManager.getSize() - numStaticActions;
+ if (n > 0) {
+ IContributionItem[] items = mMenuManager.getItems();
+ for (int i = 0; i < n; i++) {
+ mMenuManager.remove(items[i]);
+ }
+ }
+
+ // Now add all the dynamic menu actions depending on the current selection.
+ populateDynamicContextMenu();
+ }
+ });
+
+ }
+
+ /**
+ * This method is invoked by <code>menuAboutToShow</code> on {@link #mMenuManager}.
+ * All previous dynamic menu actions have been removed and this method can now insert
+ * any new actions that depend on the current selection.
+ */
+ private void populateDynamicContextMenu() {
+ // Create the actual menu contributions
+ String endId = mMenuManager.getItems()[0].getId();
+
+ Separator sep = new Separator();
+ sep.setId("-dyn-gle-sep"); //$NON-NLS-1$
+ mMenuManager.insertBefore(endId, sep);
+ endId = sep.getId();
+
+ List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections();
+ if (selections.size() == 0) {
+ return;
+ }
+ List<INode> nodes = new ArrayList<INode>(selections.size());
+ for (SelectionItem item : selections) {
+ nodes.add(item.getNode());
+ }
+
+ List<IContributionItem> menuItems = getMenuItems(nodes);
+ for (IContributionItem menuItem : menuItems) {
+ mMenuManager.insertBefore(endId, menuItem);
+ }
+
+ insertTagSpecificMenus(endId);
+ insertVisualRefactorings(endId);
+ insertParentItems(endId);
+ }
+
+ /**
+ * Returns the list of node-specific actions applicable to the given
+ * collection of nodes
+ *
+ * @param nodes the collection of nodes to look up actions for
+ * @return a list of contribution items applicable for all the nodes
+ */
+ private List<IContributionItem> getMenuItems(List<INode> nodes) {
+ Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
+ for (INode node : nodes) {
+ List<RuleAction> actionList = getMenuActions((NodeProxy) node);
+ allActions.put(node, actionList);
+ }
+
+ Set<String> availableIds = computeApplicableActionIds(allActions);
+
+ // +10: Make room for separators too
+ List<IContributionItem> items = new ArrayList<IContributionItem>(availableIds.size() + 10);
+
+ // We'll use the actions returned by the first node. Even when there
+ // are multiple items selected, we'll use the first action, but pass
+ // the set of all selected nodes to that first action. Actions are required
+ // to work this way to facilitate multi selection and actions which apply
+ // to multiple nodes.
+ NodeProxy first = (NodeProxy) nodes.get(0);
+ List<RuleAction> firstSelectedActions = allActions.get(first);
+ String defaultId = getDefaultActionId(first);
+ for (RuleAction action : firstSelectedActions) {
+ if (!availableIds.contains(action.getId())
+ && !(action instanceof RuleAction.Separator)) {
+ // This action isn't supported by all selected items.
+ continue;
+ }
+
+ items.add(createContributionItem(action, nodes, defaultId));
+ }
+
+ return items;
+ }
+
+ private void insertParentItems(String endId) {
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 1) {
+ mMenuManager.insertBefore(endId, new Separator());
+ INode parent = selection.get(0).getNode().getParent();
+ while (parent != null) {
+ String id = parent.getStringAttr(ANDROID_URI, ATTR_ID);
+ String label;
+ if (id != null && id.length() > 0) {
+ label = BaseViewRule.stripIdPrefix(id);
+ } else {
+ // Use the view name, such as "Button", as the label
+ label = parent.getFqcn();
+ // Strip off package
+ label = label.substring(label.lastIndexOf('.') + 1);
+ }
+ mMenuManager.insertBefore(endId, new NestedParentMenu(label, parent));
+ parent = parent.getParent();
+ }
+ mMenuManager.insertBefore(endId, new Separator());
+ }
+ }
+
+ private void insertVisualRefactorings(String endId) {
+ // Extract As <include> refactoring, Wrap In Refactoring, etc.
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
+ // Only include the menu item if you are not right clicking on a root,
+ // or on an included view, or on a non-contiguous selection
+ mMenuManager.insertBefore(endId, new Separator());
+ if (selection.size() == 1 && selection.get(0).getViewInfo() != null
+ && selection.get(0).getViewInfo().getName().equals(FQCN_LINEAR_LAYOUT)) {
+ CanvasViewInfo info = selection.get(0).getViewInfo();
+ List<CanvasViewInfo> children = info.getChildren();
+ if (children.size() == 2) {
+ String first = children.get(0).getName();
+ String second = children.get(1).getName();
+ if ((first.equals(FQCN_IMAGE_VIEW) && second.equals(FQCN_TEXT_VIEW))
+ || (first.equals(FQCN_TEXT_VIEW) && second.equals(FQCN_IMAGE_VIEW))) {
+ mMenuManager.insertBefore(endId, UseCompoundDrawableAction.create(
+ mEditorDelegate));
+ }
+ }
+ }
+ mMenuManager.insertBefore(endId, ExtractIncludeAction.create(mEditorDelegate));
+ mMenuManager.insertBefore(endId, ExtractStyleAction.create(mEditorDelegate));
+ mMenuManager.insertBefore(endId, WrapInAction.create(mEditorDelegate));
+ if (selection.size() == 1 && !(selection.get(0).isRoot())) {
+ mMenuManager.insertBefore(endId, UnwrapAction.create(mEditorDelegate));
+ }
+ if (selection.size() == 1 && (selection.get(0).isLayout() ||
+ selection.get(0).getViewInfo().getName().equals(FQCN_GESTURE_OVERLAY_VIEW))) {
+ mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditorDelegate));
+ } else {
+ mMenuManager.insertBefore(endId, ChangeViewAction.create(mEditorDelegate));
+ }
+ mMenuManager.insertBefore(endId, new Separator());
+ }
+
+ /** "Preview List Content" pull-right menu for lists, "Preview Fragment" for fragments, etc. */
+ private void insertTagSpecificMenus(String endId) {
+
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
+ for (SelectionItem item : selection) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ String name = node.getDescriptor().getXmlLocalName();
+ boolean isGrid = name.equals(GRID_VIEW);
+ boolean isSpinner = name.equals(SPINNER);
+ if (name.equals(LIST_VIEW) || name.equals(EXPANDABLE_LIST_VIEW)
+ || isGrid || isSpinner) {
+ mMenuManager.insertBefore(endId, new Separator());
+ mMenuManager.insertBefore(endId, new ListViewTypeMenu(mCanvas, isGrid, isSpinner));
+ return;
+ } else if (name.equals(VIEW_FRAGMENT) && selection.size() == 1) {
+ mMenuManager.insertBefore(endId, new Separator());
+ mMenuManager.insertBefore(endId, new FragmentMenu(mCanvas));
+ return;
+ }
+ }
+ }
+
+ /**
+ * Given a map from selection items to list of applicable actions (produced
+ * by {@link #computeApplicableActions()}) this method computes the set of
+ * common actions and returns the action ids of these actions.
+ *
+ * @param actions a map from selection item to list of actions applicable to
+ * that selection item
+ * @return set of action ids for the actions that are present in the action
+ * lists for all selected items
+ */
+ private Set<String> computeApplicableActionIds(Map<INode, List<RuleAction>> actions) {
+ if (actions.size() > 1) {
+ // More than one view is selected, so we have to filter down the available
+ // actions such that only those actions that are defined for all the views
+ // are shown
+ Map<String, Integer> idCounts = new HashMap<String, Integer>();
+ for (Map.Entry<INode, List<RuleAction>> entry : actions.entrySet()) {
+ List<RuleAction> actionList = entry.getValue();
+ for (RuleAction action : actionList) {
+ if (!action.supportsMultipleNodes()) {
+ continue;
+ }
+ String id = action.getId();
+ if (id != null) {
+ assert id != null : action;
+ Integer count = idCounts.get(id);
+ if (count == null) {
+ idCounts.put(id, Integer.valueOf(1));
+ } else {
+ idCounts.put(id, count + 1);
+ }
+ }
+ }
+ }
+ Integer selectionCount = Integer.valueOf(actions.size());
+ Set<String> validIds = new HashSet<String>(idCounts.size());
+ for (Map.Entry<String, Integer> entry : idCounts.entrySet()) {
+ Integer count = entry.getValue();
+ if (selectionCount.equals(count)) {
+ String id = entry.getKey();
+ validIds.add(id);
+ }
+ }
+ return validIds;
+ } else {
+ List<RuleAction> actionList = actions.values().iterator().next();
+ Set<String> validIds = new HashSet<String>(actionList.size());
+ for (RuleAction action : actionList) {
+ String id = action.getId();
+ validIds.add(id);
+ }
+ return validIds;
+ }
+ }
+
+ /**
+ * Returns the menu actions computed by the rule associated with this node.
+ *
+ * @param node the canvas node we need menu actions for
+ * @return a list of {@link RuleAction} objects applicable to the node
+ */
+ private List<RuleAction> getMenuActions(NodeProxy node) {
+ List<RuleAction> actions = mCanvas.getRulesEngine().callGetContextMenu(node);
+ if (actions == null || actions.size() == 0) {
+ return null;
+ }
+
+ return actions;
+ }
+
+ /**
+ * Returns the default action id, or null
+ *
+ * @param node the node to look up the default action for
+ * @return the action id, or null
+ */
+ private String getDefaultActionId(NodeProxy node) {
+ return mCanvas.getRulesEngine().callGetDefaultActionId(node);
+ }
+
+ /**
+ * Creates a {@link ContributionItem} for the given {@link RuleAction}.
+ *
+ * @param action the action to create a {@link ContributionItem} for
+ * @param nodes the set of nodes the action should be applied to
+ * @param defaultId if not non null, the id of an action which should be considered default
+ * @return a new {@link ContributionItem} which implements the given action
+ * on the given nodes
+ */
+ private ContributionItem createContributionItem(final RuleAction action,
+ final List<INode> nodes, final String defaultId) {
+ if (action instanceof RuleAction.Separator) {
+ return new Separator();
+ } else if (action instanceof NestedAction) {
+ NestedAction parentAction = (NestedAction) action;
+ return new ActionContributionItem(new NestedActionMenu(parentAction, nodes));
+ } else if (action instanceof Choices) {
+ Choices parentAction = (Choices) action;
+ return new ActionContributionItem(new NestedChoiceMenu(parentAction, nodes));
+ } else if (action instanceof Toggle) {
+ return new ActionContributionItem(createToggleAction(action, nodes));
+ } else {
+ return new ActionContributionItem(createPlainAction(action, nodes, defaultId));
+ }
+ }
+
+ private Action createToggleAction(final RuleAction action, final List<INode> nodes) {
+ Toggle toggleAction = (Toggle) action;
+ final boolean isChecked = toggleAction.isChecked();
+ Action a = new Action(action.getTitle(), IAction.AS_CHECK_BOX) {
+ @Override
+ public void run() {
+ String label = createActionLabel(action, nodes);
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, nodes,
+ null/* no valueId for a toggle */, !isChecked);
+ applyPendingChanges();
+ }
+ });
+ }
+ };
+ a.setId(action.getId());
+ a.setChecked(isChecked);
+ return a;
+ }
+
+ private IAction createPlainAction(final RuleAction action, final List<INode> nodes,
+ final String defaultId) {
+ IAction a = new Action(action.getTitle(), IAction.AS_PUSH_BUTTON) {
+ @Override
+ public void run() {
+ String label = createActionLabel(action, nodes);
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, nodes, null,
+ Boolean.TRUE);
+ applyPendingChanges();
+ }
+ });
+ }
+ };
+
+ String id = action.getId();
+ if (defaultId != null && id.equals(defaultId)) {
+ a.setAccelerator(DEFAULT_ACTION_KEY);
+ String text = a.getText();
+ text = text + '\t' + DEFAULT_ACTION_SHORTCUT;
+ a.setText(text);
+
+ } else if (ATTR_ID.equals(id)) {
+ // Keep in sync with {@link LayoutCanvas#handleKeyPressed}
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ a.setAccelerator('R' | SWT.MOD1 | SWT.MOD3);
+ // Option+Command
+ a.setText(a.getText().trim() + "\t\u2325\u2318R"); //$NON-NLS-1$
+ } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX) {
+ a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
+ a.setText(a.getText() + "\tShift+Alt+R"); //$NON-NLS-1$
+ } else {
+ a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
+ a.setText(a.getText() + "\tAlt+Shift+R"); //$NON-NLS-1$
+ }
+ }
+ a.setId(id);
+ return a;
+ }
+
+ private static String createActionLabel(final RuleAction action, final List<INode> nodes) {
+ String label = action.getTitle();
+ if (nodes.size() > 1) {
+ label += String.format(" (%d elements)", nodes.size());
+ }
+ return label;
+ }
+
+ /**
+ * The {@link NestedParentMenu} provides submenu content which adds actions
+ * available on one of the selected node's parent nodes. This will be
+ * similar to the menu content for the selected node, except the parent
+ * menus will not be embedded within the nested menu.
+ */
+ private class NestedParentMenu extends SubmenuAction {
+ INode mParent;
+
+ NestedParentMenu(String title, INode parent) {
+ super(title);
+ mParent = parent;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
+
+ List<IContributionItem> menuItems = getMenuItems(Collections.singletonList(mParent));
+ for (IContributionItem menuItem : menuItems) {
+ menuItem.fill(menu, -1);
+ }
+ }
+ }
+
+ /**
+ * The {@link NestedActionMenu} creates a lazily populated pull-right menu
+ * where the children are {@link RuleAction}'s themselves.
+ */
+ private class NestedActionMenu extends SubmenuAction {
+ private final NestedAction mParentAction;
+ private final List<INode> mNodes;
+
+ NestedActionMenu(NestedAction parentAction, List<INode> nodes) {
+ super(parentAction.getTitle());
+ mParentAction = parentAction;
+ mNodes = nodes;
+
+ assert mNodes.size() > 0;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
+ for (INode node : mNodes) {
+ List<RuleAction> actionList = mParentAction.getNestedActions(node);
+ allActions.put(node, actionList);
+ }
+
+ Set<String> availableIds = computeApplicableActionIds(allActions);
+
+ NodeProxy first = (NodeProxy) mNodes.get(0);
+ String defaultId = getDefaultActionId(first);
+ List<RuleAction> firstSelectedActions = allActions.get(first);
+
+ int count = 0;
+ for (RuleAction firstAction : firstSelectedActions) {
+ if (!availableIds.contains(firstAction.getId())
+ && !(firstAction instanceof RuleAction.Separator)) {
+ // This action isn't supported by all selected items.
+ continue;
+ }
+
+ createContributionItem(firstAction, mNodes, defaultId).fill(menu, -1);
+ count++;
+ }
+
+ if (count == 0) {
+ addDisabledMessageItem("<Empty>");
+ }
+ }
+ }
+
+ private void applyPendingChanges() {
+ LayoutCanvas canvas = mEditorDelegate.getGraphicalEditor().getCanvasControl();
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+
+ /**
+ * The {@link NestedChoiceMenu} creates a lazily populated pull-right menu
+ * where the items in the menu are strings
+ */
+ private class NestedChoiceMenu extends SubmenuAction {
+ private final Choices mParentAction;
+ private final List<INode> mNodes;
+
+ NestedChoiceMenu(Choices parentAction, List<INode> nodes) {
+ super(parentAction.getTitle());
+ mParentAction = parentAction;
+ mNodes = nodes;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ List<String> titles = mParentAction.getTitles();
+ List<String> ids = mParentAction.getIds();
+ String current = mParentAction.getCurrent();
+ assert titles.size() == ids.size();
+ String[] currentValues = current != null
+ && current.indexOf(RuleAction.CHOICE_SEP) != -1 ?
+ current.split(RuleAction.CHOICE_SEP_PATTERN) : null;
+ for (int i = 0, n = Math.min(titles.size(), ids.size()); i < n; i++) {
+ final String id = ids.get(i);
+ if (id == null || id.equals(RuleAction.SEPARATOR)) {
+ new Separator().fill(menu, -1);
+ continue;
+ }
+
+ // Find out whether this item is selected
+ boolean select = false;
+ if (current != null) {
+ // The current choice has a separator, so it's a flag with
+ // multiple values selected. Compare keys with the split
+ // values.
+ if (currentValues != null) {
+ if (current.indexOf(id) >= 0) {
+ for (String value : currentValues) {
+ if (id.equals(value)) {
+ select = true;
+ break;
+ }
+ }
+ }
+ } else {
+ // current choice has no separator, simply compare to the key
+ select = id.equals(current);
+ }
+ }
+
+ String title = titles.get(i);
+ IAction a = new Action(title,
+ current != null ? IAction.AS_CHECK_BOX : IAction.AS_PUSH_BUTTON) {
+ @Override
+ public void runWithEvent(Event event) {
+ run();
+ }
+ @Override
+ public void run() {
+ String label = createActionLabel(mParentAction, mNodes);
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ mParentAction.getCallback().action(mParentAction, mNodes, id,
+ Boolean.TRUE);
+ applyPendingChanges();
+ }
+ });
+ }
+ };
+ a.setId(id);
+ a.setEnabled(true);
+ if (select) {
+ a.setChecked(true);
+ }
+
+ new ActionContributionItem(a).fill(menu, -1);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java
new file mode 100644
index 000000000..daa3e0eae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link EmptyViewsOverlay} paints bounding rectangles for any of the empty and
+ * invisible container views in the scene.
+ */
+public class EmptyViewsOverlay extends Overlay {
+ /** The {@link ViewHierarchy} containing visible view information. */
+ private final ViewHierarchy mViewHierarchy;
+
+ /** Border color to paint the bounding boxes with. */
+ private Color mBorderColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Constructs a new {@link EmptyViewsOverlay} linked to the given view hierarchy.
+ *
+ * @param viewHierarchy The {@link ViewHierarchy} to render.
+ * @param hScale The {@link CanvasTransform} to use to transfer horizontal layout
+ * coordinates to screen coordinates.
+ * @param vScale The {@link CanvasTransform} to use to transfer vertical layout coordinates
+ * to screen coordinates.
+ */
+ public EmptyViewsOverlay(
+ ViewHierarchy viewHierarchy,
+ CanvasTransform hScale,
+ CanvasTransform vScale) {
+ super();
+ mViewHierarchy = viewHierarchy;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ mBorderColor = new Color(device, SwtDrawingStyle.EMPTY.getStrokeColor());
+ }
+
+ @Override
+ public void dispose() {
+ if (mBorderColor != null) {
+ mBorderColor.dispose();
+ mBorderColor = null;
+ }
+ }
+
+ @Override
+ public void paint(GC gc) {
+ gc.setForeground(mBorderColor);
+ gc.setLineDash(null);
+ gc.setLineStyle(SwtDrawingStyle.EMPTY.getLineStyle());
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(SwtDrawingStyle.EMPTY.getStrokeAlpha());
+ gc.setLineWidth(SwtDrawingStyle.EMPTY.getLineWidth());
+
+ for (CanvasViewInfo info : mViewHierarchy.getInvisibleViews()) {
+ Rectangle r = info.getAbsRect();
+
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.width);
+ int h = mVScale.scale(r.height);
+
+ // +1: See explanation in equivalent code in {@link OutlineOverlay#paint}
+ gc.drawRectangle(x, y, w + 1, h + 1);
+ }
+
+ gc.setAlpha(oldAlpha);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java
new file mode 100644
index 000000000..ac3328db2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.DOT_PNG;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+/** Saves the current layout editor's rendered image to disk */
+class ExportScreenshotAction extends Action {
+ private final LayoutCanvas mCanvas;
+
+ ExportScreenshotAction(LayoutCanvas canvas) {
+ super("Export Screenshot...");
+ mCanvas = canvas;
+ }
+
+ @Override
+ public void run() {
+ Shell shell = AdtPlugin.getShell();
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ BufferedImage image = imageOverlay.getAwtImage();
+ if (image != null) {
+ FileDialog dialog = new FileDialog(shell, SWT.SAVE);
+ dialog.setFilterExtensions(new String[] { "*.png" }); //$NON-NLS-1$
+ String path = dialog.open();
+ if (path != null) {
+ if (!path.endsWith(DOT_PNG)) {
+ path = path + DOT_PNG;
+ }
+ File file = new File(path);
+ if (file.exists()) {
+ MessageDialog d = new MessageDialog(null, "File Already Exists", null,
+ String.format(
+ "%1$s already exists.\nWould you like to replace it?",
+ path),
+ MessageDialog.QUESTION, new String[] {
+ // Yes will be moved to the end because it's the default
+ "Yes", "No"
+ }, 0);
+ int result = d.open();
+ if (result != 0) {
+ return;
+ }
+ }
+ try {
+ ImageIO.write(image, "PNG", file); //$NON-NLS-1$
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ } else {
+ MessageDialog.openError(shell, "Error", "Image not available");
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java
new file mode 100644
index 000000000..f7085fc12
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_FRAGMENT_LAYOUT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Menu;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fragment context menu allowing a layout to be chosen for previewing in the fragment frame.
+ */
+public class FragmentMenu extends SubmenuAction {
+ private static final String R_LAYOUT_RESOURCE_PREFIX = "R.layout."; //$NON-NLS-1$
+ private static final String ANDROID_R_PREFIX = "android.R.layout"; //$NON-NLS-1$
+
+ /** Associated canvas */
+ private final LayoutCanvas mCanvas;
+
+ /**
+ * Creates a "Preview Fragment" menu
+ *
+ * @param canvas associated canvas
+ */
+ public FragmentMenu(LayoutCanvas canvas) {
+ super("Fragment Layout");
+ mCanvas = canvas;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ IAction action = new PickLayoutAction("Choose Layout...");
+ new ActionContributionItem(action).fill(menu, -1);
+
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ if (selections.size() == 0) {
+ return;
+ }
+
+ SelectionItem first = selections.get(0);
+ UiViewElementNode node = first.getViewInfo().getUiViewNode();
+ if (node == null) {
+ return;
+ }
+ Element element = (Element) node.getXmlNode();
+
+ String selected = getSelectedLayout();
+ if (selected != null) {
+ if (selected.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
+ selected = selected.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ }
+
+ String fqcn = getFragmentClass(element);
+ if (fqcn != null) {
+ // Look up the corresponding activity class and try to figure out
+ // which layouts it is referring to and list these here as reasonable
+ // guesses
+ IProject project = mCanvas.getEditorDelegate().getEditor().getProject();
+ String source = null;
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ IType type = javaProject.findType(fqcn);
+ if (type != null) {
+ source = type.getSource();
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ // Find layouts. This is based on just skimming the Fragment class and looking
+ // for layout references of the form R.layout.*.
+ if (source != null) {
+ String self = mCanvas.getLayoutResourceName();
+ // Pair of <title,layout> to be displayed to the user
+ List<Pair<String, String>> layouts = new ArrayList<Pair<String, String>>();
+
+ if (source.contains("extends ListFragment")) { //$NON-NLS-1$
+ layouts.add(Pair.of("list_content", //$NON-NLS-1$
+ "@android:layout/list_content")); //$NON-NLS-1$
+ }
+
+ int index = 0;
+ while (true) {
+ index = source.indexOf(R_LAYOUT_RESOURCE_PREFIX, index);
+ if (index == -1) {
+ break;
+ } else {
+ index += R_LAYOUT_RESOURCE_PREFIX.length();
+ int end = index;
+ while (end < source.length()) {
+ char c = source.charAt(end);
+ if (!Character.isJavaIdentifierPart(c)) {
+ break;
+ }
+ end++;
+ }
+ if (end > index) {
+ String title = source.substring(index, end);
+ String layout;
+ // Is this R.layout part of an android.R.layout?
+ int len = ANDROID_R_PREFIX.length() + 1; // prefix length to check
+ if (index > len && source.startsWith(ANDROID_R_PREFIX, index - len)) {
+ layout = ANDROID_LAYOUT_RESOURCE_PREFIX + title;
+ } else {
+ layout = LAYOUT_RESOURCE_PREFIX + title;
+ }
+ if (!self.equals(title)) {
+ layouts.add(Pair.of(title, layout));
+ }
+ }
+ }
+
+ index++;
+ }
+
+ if (layouts.size() > 0) {
+ new Separator().fill(menu, -1);
+ for (Pair<String, String> layout : layouts) {
+ action = new SetFragmentLayoutAction(layout.getFirst(),
+ layout.getSecond(), selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ }
+ }
+ }
+
+ if (selected != null) {
+ new Separator().fill(menu, -1);
+ action = new SetFragmentLayoutAction("Clear", null, null);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ }
+
+ /**
+ * Returns the class name of the fragment associated with the given {@code <fragment>}
+ * element.
+ *
+ * @param element the element for the fragment tag
+ * @return the fully qualified fragment class name, or null
+ */
+ @Nullable
+ public static String getFragmentClass(@NonNull Element element) {
+ String fqcn = element.getAttribute(ATTR_CLASS);
+ if (fqcn == null || fqcn.length() == 0) {
+ fqcn = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
+ }
+ if (fqcn != null && fqcn.length() > 0) {
+ return fqcn;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the layout to be shown for the given {@code <fragment>} node.
+ *
+ * @param node the node corresponding to the {@code <fragment>} element
+ * @return the resource path to a layout to render for this fragment, or null
+ */
+ @Nullable
+ public static String getFragmentLayout(@NonNull Node node) {
+ String layout = LayoutMetadata.getProperty(
+ node, LayoutMetadata.KEY_FRAGMENT_LAYOUT);
+ if (layout != null) {
+ return layout;
+ }
+
+ return null;
+ }
+
+ /** Returns the name of the currently displayed layout in the fragment, or null */
+ @Nullable
+ private String getSelectedLayout() {
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ for (SelectionItem item : selectionManager.getSelections()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ String layout = getFragmentLayout(node.getXmlNode());
+ if (layout != null) {
+ return layout;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set the given layout as the new fragment layout
+ *
+ * @param layout the layout resource name to show in this fragment
+ */
+ public void setNewLayout(@Nullable String layout) {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor();
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ for (SelectionItem item : selectionManager.getSnapshot()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ Node xmlNode = node.getXmlNode();
+ LayoutMetadata.setProperty(delegate.getEditor(), xmlNode, KEY_FRAGMENT_LAYOUT,
+ layout);
+ }
+ }
+
+ // Refresh
+ graphicalEditor.recomputeLayout();
+ mCanvas.redraw();
+ }
+
+ /** Action to set the given layout as the new layout in a fragment */
+ private class SetFragmentLayoutAction extends Action {
+ private final String mLayout;
+
+ public SetFragmentLayoutAction(String title, String layout, String selected) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mLayout = layout;
+
+ if (layout != null && layout.equals(selected)) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (isChecked()) {
+ setNewLayout(mLayout);
+ }
+ }
+ }
+
+ /**
+ * Action which brings up the "Create new XML File" wizard, pre-selected with the
+ * animation category
+ */
+ private class PickLayoutAction extends Action {
+
+ public PickLayoutAction(String title) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ }
+
+ @Override
+ public void run() {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ IFile file = delegate.getEditor().getInputFile();
+ GraphicalEditorPart editor = delegate.getGraphicalEditor();
+ ResourceChooser dlg = ResourceChooser.create(editor, ResourceType.LAYOUT)
+ .setInputValidator(CyclicDependencyValidator.create(file))
+ .setInitialSize(85, 10)
+ .setCurrentResource(getSelectedLayout());
+ int result = dlg.open();
+ if (result == ResourceChooser.CLEAR_RETURN_CODE) {
+ setNewLayout(null);
+ } else if (result == Window.OK) {
+ String newType = dlg.getCurrentResource();
+ setNewLayout(newType);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java
new file mode 100644
index 000000000..354517e76
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.IColor;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.RGB;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects
+ * can directly draw on the canvas.
+ * <p/>
+ * The actual wrapped GC object is only non-null during the context of a paint operation.
+ */
+public class GCWrapper implements IGraphics {
+
+ /**
+ * The actual SWT {@link GC} being wrapped. This can change during the lifetime of the
+ * object. It is generally set to something during an onPaint method and then changed
+ * to null when not in the context of a paint.
+ */
+ private GC mGc;
+
+ /**
+ * Current style being used for drawing.
+ */
+ private SwtDrawingStyle mCurrentStyle = SwtDrawingStyle.INVALID;
+
+ /**
+ * Implementation of IColor wrapping an SWT color.
+ */
+ private static class ColorWrapper implements IColor {
+ private final Color mColor;
+
+ public ColorWrapper(Color color) {
+ mColor = color;
+ }
+
+ public Color getColor() {
+ return mColor;
+ }
+ }
+
+ /** A map of registered colors. All these colors must be disposed at the end. */
+ private final HashMap<Integer, ColorWrapper> mColorMap = new HashMap<Integer, ColorWrapper>();
+
+ /**
+ * A map of the {@link SwtDrawingStyle} stroke colors that we have actually
+ * used (to be disposed)
+ */
+ private final Map<DrawingStyle, Color> mStyleStrokeMap = new EnumMap<DrawingStyle, Color>(
+ DrawingStyle.class);
+
+ /**
+ * A map of the {@link SwtDrawingStyle} fill colors that we have actually
+ * used (to be disposed)
+ */
+ private final Map<DrawingStyle, Color> mStyleFillMap = new EnumMap<DrawingStyle, Color>(
+ DrawingStyle.class);
+
+ /** The cached pixel height of the default current font. */
+ private int mFontHeight = 0;
+
+ /** The scaling of the canvas in X. */
+ private final CanvasTransform mHScale;
+ /** The scaling of the canvas in Y. */
+ private final CanvasTransform mVScale;
+
+ public GCWrapper(CanvasTransform hScale, CanvasTransform vScale) {
+ mHScale = hScale;
+ mVScale = vScale;
+ mGc = null;
+ }
+
+ void setGC(GC gc) {
+ mGc = gc;
+ }
+
+ private GC getGc() {
+ return mGc;
+ }
+
+ void checkGC() {
+ if (mGc == null) {
+ throw new RuntimeException("IGraphics used without a valid context.");
+ }
+ }
+
+ void dispose() {
+ for (ColorWrapper c : mColorMap.values()) {
+ c.getColor().dispose();
+ }
+ mColorMap.clear();
+
+ for (Color c : mStyleStrokeMap.values()) {
+ c.dispose();
+ }
+ mStyleStrokeMap.clear();
+
+ for (Color c : mStyleFillMap.values()) {
+ c.dispose();
+ }
+ mStyleFillMap.clear();
+ }
+
+ //-------------
+
+ @Override
+ public @NonNull IColor registerColor(int rgb) {
+ checkGC();
+
+ Integer key = Integer.valueOf(rgb);
+ ColorWrapper c = mColorMap.get(key);
+ if (c == null) {
+ c = new ColorWrapper(new Color(getGc().getDevice(),
+ (rgb >> 16) & 0xFF,
+ (rgb >> 8) & 0xFF,
+ (rgb >> 0) & 0xFF));
+ mColorMap.put(key, c);
+ }
+
+ return c;
+ }
+
+ /** Returns the (cached) pixel height of the current font. */
+ @Override
+ public int getFontHeight() {
+ if (mFontHeight < 1) {
+ checkGC();
+ FontMetrics fm = getGc().getFontMetrics();
+ mFontHeight = fm.getHeight();
+ }
+ return mFontHeight;
+ }
+
+ @Override
+ public @NonNull IColor getForeground() {
+ Color c = getGc().getForeground();
+ return new ColorWrapper(c);
+ }
+
+ @Override
+ public @NonNull IColor getBackground() {
+ Color c = getGc().getBackground();
+ return new ColorWrapper(c);
+ }
+
+ @Override
+ public int getAlpha() {
+ return getGc().getAlpha();
+ }
+
+ @Override
+ public void setForeground(@NonNull IColor color) {
+ checkGC();
+ getGc().setForeground(((ColorWrapper) color).getColor());
+ }
+
+ @Override
+ public void setBackground(@NonNull IColor color) {
+ checkGC();
+ getGc().setBackground(((ColorWrapper) color).getColor());
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ checkGC();
+ try {
+ getGc().setAlpha(alpha);
+ } catch (SWTException e) {
+ // This means that we cannot set the alpha on this platform; this is
+ // an acceptable no-op.
+ }
+ }
+
+ @Override
+ public void setLineStyle(@NonNull LineStyle style) {
+ int swtStyle = 0;
+ switch (style) {
+ case LINE_SOLID:
+ swtStyle = SWT.LINE_SOLID;
+ break;
+ case LINE_DASH:
+ swtStyle = SWT.LINE_DASH;
+ break;
+ case LINE_DOT:
+ swtStyle = SWT.LINE_DOT;
+ break;
+ case LINE_DASHDOT:
+ swtStyle = SWT.LINE_DASHDOT;
+ break;
+ case LINE_DASHDOTDOT:
+ swtStyle = SWT.LINE_DASHDOTDOT;
+ break;
+ default:
+ assert false : style;
+ break;
+ }
+
+ if (swtStyle != 0) {
+ checkGC();
+ getGc().setLineStyle(swtStyle);
+ }
+ }
+
+ @Override
+ public void setLineWidth(int width) {
+ checkGC();
+ if (width > 0) {
+ getGc().setLineWidth(width);
+ }
+ }
+
+ // lines
+
+ @Override
+ public void drawLine(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useStrokeAlpha();
+ x1 = mHScale.translate(x1);
+ y1 = mVScale.translate(y1);
+ x2 = mHScale.translate(x2);
+ y2 = mVScale.translate(y2);
+ getGc().drawLine(x1, y1, x2, y2);
+ }
+
+ @Override
+ public void drawLine(@NonNull Point p1, @NonNull Point p2) {
+ drawLine(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ // rectangles
+
+ @Override
+ public void drawRect(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().drawRectangle(x, y, w, h);
+ }
+
+ @Override
+ public void drawRect(@NonNull Point p1, @NonNull Point p2) {
+ drawRect(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ @Override
+ public void drawRect(@NonNull Rect r) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().drawRectangle(x, y, w, h);
+ }
+
+ @Override
+ public void fillRect(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().fillRectangle(x, y, w, h);
+ }
+
+ @Override
+ public void fillRect(@NonNull Point p1, @NonNull Point p2) {
+ fillRect(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ @Override
+ public void fillRect(@NonNull Rect r) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().fillRectangle(x, y, w, h);
+ }
+
+ // circles (actually ovals)
+
+ public void drawOval(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().drawOval(x, y, w, h);
+ }
+
+ public void drawOval(Point p1, Point p2) {
+ drawOval(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ public void drawOval(Rect r) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().drawOval(x, y, w, h);
+ }
+
+ public void fillOval(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().fillOval(x, y, w, h);
+ }
+
+ public void fillOval(Point p1, Point p2) {
+ fillOval(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ public void fillOval(Rect r) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().fillOval(x, y, w, h);
+ }
+
+
+ // strings
+
+ @Override
+ public void drawString(@NonNull String string, int x, int y) {
+ checkGC();
+ useStrokeAlpha();
+ x = mHScale.translate(x);
+ y = mVScale.translate(y);
+ // Background fill of text is not useful because it does not
+ // use the alpha; we instead supply a separate method (drawBoxedStrings) which
+ // first paints a semi-transparent mask for the text to sit on
+ // top of (this ensures that the text is readable regardless of
+ // colors of the pixels below the text)
+ getGc().drawString(string, x, y, true /*isTransparent*/);
+ }
+
+ @Override
+ public void drawBoxedStrings(int x, int y, @NonNull List<?> strings) {
+ checkGC();
+
+ x = mHScale.translate(x);
+ y = mVScale.translate(y);
+
+ // Compute bounds of the box by adding up the sum of the text heights
+ // and the max of the text widths
+ int width = 0;
+ int height = 0;
+ int lineHeight = getGc().getFontMetrics().getHeight();
+ for (Object s : strings) {
+ org.eclipse.swt.graphics.Point extent = getGc().stringExtent(s.toString());
+ height += extent.y;
+ width = Math.max(width, extent.x);
+ }
+
+ // Paint a box below the text
+ int padding = 2;
+ useFillAlpha();
+ getGc().fillRectangle(x - padding, y - padding, width + 2 * padding, height + 2 * padding);
+
+ // Finally draw strings on top
+ useStrokeAlpha();
+ int lineY = y;
+ for (Object s : strings) {
+ getGc().drawString(s.toString(), x, lineY, true /* isTransparent */);
+ lineY += lineHeight;
+ }
+ }
+
+ @Override
+ public void drawString(@NonNull String string, @NonNull Point topLeft) {
+ drawString(string, topLeft.x, topLeft.y);
+ }
+
+ // Styles
+
+ @Override
+ public void useStyle(@NonNull DrawingStyle style) {
+ checkGC();
+
+ // Look up the specific SWT style which defines the actual
+ // colors and attributes to be used for the logical drawing style.
+ SwtDrawingStyle swtStyle = SwtDrawingStyle.of(style);
+ RGB stroke = swtStyle.getStrokeColor();
+ if (stroke != null) {
+ Color color = getStrokeColor(style, stroke);
+ mGc.setForeground(color);
+ }
+ RGB fill = swtStyle.getFillColor();
+ if (fill != null) {
+ Color color = getFillColor(style, fill);
+ mGc.setBackground(color);
+ }
+ mGc.setLineWidth(swtStyle.getLineWidth());
+ mGc.setLineStyle(swtStyle.getLineStyle());
+ if (swtStyle.getLineStyle() == SWT.LINE_CUSTOM) {
+ mGc.setLineDash(new int[] {
+ 8, 4
+ });
+ }
+ mCurrentStyle = swtStyle;
+ }
+
+ /** Uses the stroke alpha for subsequent drawing operations. */
+ private void useStrokeAlpha() {
+ mGc.setAlpha(mCurrentStyle.getStrokeAlpha());
+ }
+
+ /** Uses the fill alpha for subsequent drawing operations. */
+ private void useFillAlpha() {
+ mGc.setAlpha(mCurrentStyle.getFillAlpha());
+ }
+
+ /**
+ * Get the SWT stroke color (foreground/border) to use for the given style,
+ * using the provided color description if we haven't seen this color yet.
+ * The color will also be placed in the {@link #mStyleStrokeMap} such that
+ * it can be disposed of at cleanup time.
+ *
+ * @param style The drawing style for which we want a color
+ * @param defaultColorDesc The RGB values to initialize the color to if we
+ * haven't seen this color before
+ * @return The color object
+ */
+ private Color getStrokeColor(DrawingStyle style, RGB defaultColorDesc) {
+ return getStyleColor(style, defaultColorDesc, mStyleStrokeMap);
+ }
+
+ /**
+ * Get the SWT fill (background/interior) color to use for the given style,
+ * using the provided color description if we haven't seen this color yet.
+ * The color will also be placed in the {@link #mStyleStrokeMap} such that
+ * it can be disposed of at cleanup time.
+ *
+ * @param style The drawing style for which we want a color
+ * @param defaultColorDesc The RGB values to initialize the color to if we
+ * haven't seen this color before
+ * @return The color object
+ */
+ private Color getFillColor(DrawingStyle style, RGB defaultColorDesc) {
+ return getStyleColor(style, defaultColorDesc, mStyleFillMap);
+ }
+
+ /**
+ * Get the SWT color to use for the given style, using the provided color
+ * description if we haven't seen this color yet. The color will also be
+ * placed in the map referenced by the map parameter such that it can be
+ * disposed of at cleanup time.
+ *
+ * @param style The drawing style for which we want a color
+ * @param defaultColorDesc The RGB values to initialize the color to if we
+ * haven't seen this color before
+ * @param map The color map to use
+ * @return The color object
+ */
+ private Color getStyleColor(DrawingStyle style, RGB defaultColorDesc,
+ Map<DrawingStyle, Color> map) {
+ Color color = map.get(style);
+ if (color == null) {
+ color = new Color(getGc().getDevice(), defaultColorDesc);
+ map.put(style, color);
+ }
+
+ return color;
+ }
+
+ // dots
+
+ @Override
+ public void drawPoint(int x, int y) {
+ checkGC();
+ useStrokeAlpha();
+ x = mHScale.translate(x);
+ y = mVScale.translate(y);
+
+ getGc().drawPoint(x, y);
+ }
+
+ // arrows
+
+ private static final int MIN_LENGTH = 10;
+
+
+ @Override
+ public void drawArrow(int x1, int y1, int x2, int y2, int size) {
+ int arrowWidth = size;
+ int arrowHeight = size;
+
+ checkGC();
+ useStrokeAlpha();
+ x1 = mHScale.translate(x1);
+ y1 = mVScale.translate(y1);
+ x2 = mHScale.translate(x2);
+ y2 = mVScale.translate(y2);
+ GC graphics = getGc();
+
+ // Make size adjustments to ensure that the arrow has enough width to be visible
+ if (x1 == x2 && Math.abs(y1 - y2) < MIN_LENGTH) {
+ int delta = (MIN_LENGTH - Math.abs(y1 - y2)) / 2;
+ if (y1 < y2) {
+ y1 -= delta;
+ y2 += delta;
+ } else {
+ y1 += delta;
+ y2-= delta;
+ }
+
+ } else if (y1 == y2 && Math.abs(x1 - x2) < MIN_LENGTH) {
+ int delta = (MIN_LENGTH - Math.abs(x1 - x2)) / 2;
+ if (x1 < x2) {
+ x1 -= delta;
+ x2 += delta;
+ } else {
+ x1 += delta;
+ x2-= delta;
+ }
+ }
+
+ graphics.drawLine(x1, y1, x2, y2);
+
+ // Arrowhead:
+
+ if (x1 == x2) {
+ // Vertical
+ if (y2 > y1) {
+ graphics.drawLine(x2 - arrowWidth, y2 - arrowHeight, x2, y2);
+ graphics.drawLine(x2 + arrowWidth, y2 - arrowHeight, x2, y2);
+ } else {
+ graphics.drawLine(x2 - arrowWidth, y2 + arrowHeight, x2, y2);
+ graphics.drawLine(x2 + arrowWidth, y2 + arrowHeight, x2, y2);
+ }
+ } else if (y1 == y2) {
+ // Horizontal
+ if (x2 > x1) {
+ graphics.drawLine(x2 - arrowHeight, y2 - arrowWidth, x2, y2);
+ graphics.drawLine(x2 - arrowHeight, y2 + arrowWidth, x2, y2);
+ } else {
+ graphics.drawLine(x2 + arrowHeight, y2 - arrowWidth, x2, y2);
+ graphics.drawLine(x2 + arrowHeight, y2 + arrowWidth, x2, y2);
+ }
+ } else {
+ // Compute angle:
+ int dy = y2 - y1;
+ int dx = x2 - x1;
+ double angle = Math.atan2(dy, dx);
+ double lineLength = Math.sqrt(dy * dy + dx * dx);
+
+ // Imagine a line of the same length as the arrow, but with angle 0.
+ // Its two arrow lines are at (-arrowWidth, -arrowHeight) relative
+ // to the endpoint (x1 + lineLength, y1) stretching up to (x2,y2).
+ // We compute the positions of (ax,ay) for the point above and
+ // below this line and paint the lines to it:
+ double ax = x1 + lineLength - arrowHeight;
+ double ay = y1 - arrowWidth;
+ int rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1);
+ int ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1);
+ graphics.drawLine(x2, y2, rx, ry);
+
+ ay = y1 + arrowWidth;
+ rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1);
+ ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1);
+ graphics.drawLine(x2, y2, rx, ry);
+ }
+
+ /* TODO: Experiment with filled arrow heads?
+ if (x1 == x2) {
+ // Vertical
+ if (y2 > y1) {
+ for (int i = 0; i < arrowWidth; i++) {
+ graphics.drawLine(x2 - arrowWidth + i, y2 - arrowWidth + i,
+ x2 + arrowWidth - i, y2 - arrowWidth + i);
+ }
+ } else {
+ for (int i = 0; i < arrowWidth; i++) {
+ graphics.drawLine(x2 - arrowWidth + i, y2 + arrowWidth - i,
+ x2 + arrowWidth - i, y2 + arrowWidth - i);
+ }
+ }
+ } else if (y1 == y2) {
+ // Horizontal
+ if (x2 > x1) {
+ for (int i = 0; i < arrowHeight; i++) {
+ graphics.drawLine(x2 - arrowHeight + i, y2 - arrowHeight + i, x2
+ - arrowHeight + i, y2 + arrowHeight - i);
+ }
+ } else {
+ for (int i = 0; i < arrowHeight; i++) {
+ graphics.drawLine(x2 + arrowHeight - i, y2 - arrowHeight + i, x2
+ + arrowHeight - i, y2 + arrowHeight - i);
+ }
+ }
+ } else {
+ // Arbitrary angle -- need to use trig
+ // TODO: Implement this
+ }
+ */
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java
new file mode 100644
index 000000000..a35d19078
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.utils.Pair;
+
+import org.eclipse.swt.events.KeyEvent;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A gesture is a mouse or keyboard driven user operation, such as a
+ * swipe-select or a resize. It can be thought of as a session, since it is
+ * initiated, updated during user manipulation, and finally completed or
+ * canceled. A gesture is associated with a single undo transaction (although
+ * some gestures don't actually edit anything, such as a selection), and a
+ * gesture can have a number of graphics {@link Overlay}s which are added and
+ * cleaned up on behalf of the gesture by the system.
+ * <p/>
+ * Gestures are typically mouse oriented. If a mouse wishes to integrate
+ * with the native drag &amp; drop support, it should also implement
+ * the {@link DropGesture} interface, which is a sub interface of this
+ * {@link Gesture} interface. There are pros and cons to using native drag
+ * &amp; drop, so various gestures will differ in whether they use it.
+ * In particular, you should use drag &amp; drop if your gesture should:
+ * <ul>
+ * <li> Show a native drag &amp; drop cursor
+ * <li> Copy or move data, especially if this applies outside the canvas
+ * control window or even the application itself
+ * </ul>
+ * You might want to avoid using native drag &amp; drop if your gesture should:
+ * <ul>
+ * <li> Continue updating itself even when the mouse cursor leaves the
+ * canvas window (in a drag &amp; gesture, as soon as you leave the canvas
+ * the drag source is no longer informed of mouse updates, whereas a regular
+ * mouse listener is)
+ * <li> Respond to modifier keys (for example, if toggling the Shift key
+ * should constrain motion as is common during resizing, and so on)
+ * <li> Use no special cursor (for example, during a marquee selection gesture we
+ * don't want a native drag &amp; drop cursor)
+ * </ul>
+ * <p/>
+ * Examples of gestures:
+ * <ul>
+ * <li>Move (dragging to reorder or change hierarchy of views or change visual
+ * layout attributes)
+ * <li>Marquee (swiping out a rectangle to make a selection)
+ * <li>Resize (dragging some edge or corner of a widget to change its size, for
+ * example to some new fixed size, or to "attach" it to some other edge.)
+ * <li>Inline Editing (editing the text of some text-oriented widget like a
+ * label or a button)
+ * <li>Link (associate two or more widgets in some way, such as an
+ * "is required" widget linked to a text field)
+ * </ul>
+ */
+public abstract class Gesture {
+ /** Start mouse coordinate, in control coordinates. */
+ protected ControlPoint mStart;
+
+ /** Initial SWT mask when the gesture started. */
+ protected int mStartMask;
+
+ /**
+ * Returns a list of overlays, from bottom to top (where the later overlays
+ * are painted on top of earlier ones if they overlap).
+ *
+ * @return A list of overlays to paint for this gesture, if applicable.
+ * Should not be null, but can be empty.
+ */
+ public List<Overlay> createOverlays() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Handles initialization of this gesture. Called when the gesture is
+ * starting.
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ * @param startMask The initial SWT mask for the gesture, if known, or
+ * otherwise 0.
+ */
+ public void begin(ControlPoint pos, int startMask) {
+ mStart = pos;
+ mStartMask = startMask;
+ }
+
+ /**
+ * Handles updating of the gesture state for a new mouse position.
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ */
+ public void update(ControlPoint pos) {
+ }
+
+ /**
+ * Handles termination of the gesture. This method is called when the
+ * gesture has terminated (either through successful completion, or because
+ * it was canceled).
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ * @param canceled True if the gesture was canceled, and false otherwise.
+ */
+ public void end(ControlPoint pos, boolean canceled) {
+ }
+
+ /**
+ * Handles a key press during the gesture. May be called repeatedly when the
+ * user is holding the key for several seconds.
+ *
+ * @param event The SWT event for the key press,
+ * @return true if this gesture consumed the key press, otherwise return false
+ */
+ public boolean keyPressed(KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Handles a key release during the gesture.
+ *
+ * @param event The SWT event for the key release,
+ * @return true if this gesture consumed the key press, otherwise return false
+ */
+ public boolean keyReleased(KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Returns whether tooltips should be display below and to the right of the mouse
+ * cursor.
+ *
+ * @return a pair of booleans, the first indicating whether the tooltip should be
+ * below and the second indicating whether the tooltip should be displayed to
+ * the right of the mouse cursor.
+ */
+ public Pair<Boolean, Boolean> getTooltipPosition() {
+ return Pair.of(true, true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java
new file mode 100644
index 000000000..98bc25e37
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java
@@ -0,0 +1,930 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.TypedEvent;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorSite;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link GestureManager} is is the central manager of gestures; it is responsible
+ * for recognizing when particular gestures should begin and terminate. It
+ * listens to the drag, mouse and keyboard systems to find out when to start
+ * gestures and in order to update the gestures along the way.
+ */
+public class GestureManager {
+ /** The canvas which owns this GestureManager. */
+ private final LayoutCanvas mCanvas;
+
+ /** The currently executing gesture, or null. */
+ private Gesture mCurrentGesture;
+
+ /** A listener for drop target events. */
+ private final DropTargetListener mDropListener = new CanvasDropListener();
+
+ /** A listener for drag source events. */
+ private final DragSourceListener mDragSourceListener = new CanvasDragSourceListener();
+
+ /** Tooltip shown during the gesture, or null */
+ private GestureToolTip mTooltip;
+
+ /**
+ * The list of overlays associated with {@link #mCurrentGesture}. Will be
+ * null before it has been initialized lazily by the paint routine (the
+ * initialized value can never be null, but it can be an empty collection).
+ */
+ private List<Overlay> mOverlays;
+
+ /**
+ * Most recently seen mouse position (x coordinate). We keep a copy of this
+ * value since we sometimes need to know it when we aren't told about the
+ * mouse position (such as when a keystroke is received, such as an arrow
+ * key in order to tweak the current drop position)
+ */
+ protected int mLastMouseX;
+
+ /**
+ * Most recently seen mouse position (y coordinate). We keep a copy of this
+ * value since we sometimes need to know it when we aren't told about the
+ * mouse position (such as when a keystroke is received, such as an arrow
+ * key in order to tweak the current drop position)
+ */
+ protected int mLastMouseY;
+
+ /**
+ * Most recently seen mouse mask. We keep a copy of this since in some
+ * scenarios (such as on a drag gesture) we don't get access to it.
+ */
+ protected int mLastStateMask;
+
+ /**
+ * Listener for mouse motion, click and keyboard events.
+ */
+ private Listener mListener;
+
+ /**
+ * When we the drag leaves, we don't know if that's the last we'll see of
+ * this drag or if it's just temporarily outside the canvas and it will
+ * return. We want to restore it if it comes back. This is also necessary
+ * because even on a drop we'll receive a
+ * {@link DropTargetListener#dragLeave} right before the drop, and we need
+ * to restore it in the drop. Therefore, when we lose a {@link DropGesture}
+ * to a {@link DropTargetListener#dragLeave}, we store a reference to the
+ * current gesture as a {@link #mZombieGesture}, since the gesture is dead
+ * but might be brought back to life if we see a subsequent
+ * {@link DropTargetListener#dragEnter} before another gesture begins.
+ */
+ private DropGesture mZombieGesture;
+
+ /**
+ * Flag tracking whether we've set a message or error message on the global status
+ * line (since we only want to clear that message if we have set it ourselves).
+ * This is the actual message rather than a boolean such that (if we can get our
+ * hands on the global message) we can check to see if the current message is the
+ * one we set and only in that case clear it when it is no longer applicable.
+ */
+ private String mDisplayingMessage;
+
+ /**
+ * Constructs a new {@link GestureManager} for the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The canvas which controls this {@link GestureManager}
+ */
+ public GestureManager(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * Returns the canvas associated with this GestureManager.
+ *
+ * @return The {@link LayoutCanvas} associated with this GestureManager.
+ * Never null.
+ */
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the current gesture, if one is in progress, and otherwise returns
+ * null.
+ *
+ * @return The current gesture or null.
+ */
+ public Gesture getCurrentGesture() {
+ return mCurrentGesture;
+ }
+
+ /**
+ * Paints the overlays associated with the current gesture, if any.
+ *
+ * @param gc The graphics object to paint into.
+ */
+ public void paint(GC gc) {
+ if (mCurrentGesture == null) {
+ return;
+ }
+
+ if (mOverlays == null) {
+ mOverlays = mCurrentGesture.createOverlays();
+ Device device = gc.getDevice();
+ for (Overlay overlay : mOverlays) {
+ overlay.create(device);
+ }
+ }
+ for (Overlay overlay : mOverlays) {
+ overlay.paint(gc);
+ }
+ }
+
+ /**
+ * Registers all the listeners needed by the {@link GestureManager}.
+ *
+ * @param dragSource The drag source in the {@link LayoutCanvas} to listen
+ * to.
+ * @param dropTarget The drop target in the {@link LayoutCanvas} to listen
+ * to.
+ */
+ public void registerListeners(DragSource dragSource, DropTarget dropTarget) {
+ assert mListener == null;
+ mListener = new Listener();
+ mCanvas.addMouseMoveListener(mListener);
+ mCanvas.addMouseListener(mListener);
+ mCanvas.addKeyListener(mListener);
+
+ if (dragSource != null) {
+ dragSource.addDragListener(mDragSourceListener);
+ }
+ if (dropTarget != null) {
+ dropTarget.addDropListener(mDropListener);
+ }
+ }
+
+ /**
+ * Unregisters all the listeners previously registered by
+ * {@link #registerListeners}.
+ *
+ * @param dragSource The drag source in the {@link LayoutCanvas} to stop
+ * listening to.
+ * @param dropTarget The drop target in the {@link LayoutCanvas} to stop
+ * listening to.
+ */
+ public void unregisterListeners(DragSource dragSource, DropTarget dropTarget) {
+ if (mCanvas.isDisposed()) {
+ // If the LayoutCanvas is already disposed, we shouldn't try to unregister
+ // the listeners; they are already not active and an attempt to remove the
+ // listener will throw a widget-is-disposed exception.
+ mListener = null;
+ return;
+ }
+
+ if (mListener != null) {
+ mCanvas.removeMouseMoveListener(mListener);
+ mCanvas.removeMouseListener(mListener);
+ mCanvas.removeKeyListener(mListener);
+ mListener = null;
+ }
+
+ if (dragSource != null) {
+ dragSource.removeDragListener(mDragSourceListener);
+ }
+ if (dropTarget != null) {
+ dropTarget.removeDropListener(mDropListener);
+ }
+ }
+
+ /**
+ * Starts the given gesture.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param gesture The gesture to initiate
+ */
+ private void startGesture(ControlPoint mousePos, Gesture gesture, int mask) {
+ if (mCurrentGesture != null) {
+ finishGesture(mousePos, true);
+ assert mCurrentGesture == null;
+ }
+
+ if (gesture != null) {
+ mCurrentGesture = gesture;
+ mCurrentGesture.begin(mousePos, mask);
+ }
+ }
+
+ /**
+ * Updates the current gesture, if any, for the given event.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param event The event corresponding to this update. May be null. Don't
+ * make any assumptions about the type of this event - for
+ * example, it may not always be a MouseEvent, it could be a
+ * DragSourceEvent, etc.
+ */
+ private void updateMouse(ControlPoint mousePos, TypedEvent event) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.update(mousePos);
+ }
+ }
+
+ /**
+ * Finish the given gesture, either from successful completion or from
+ * cancellation.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param canceled True if and only if the gesture was canceled.
+ */
+ private void finishGesture(ControlPoint mousePos, boolean canceled) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.end(mousePos, canceled);
+ if (mOverlays != null) {
+ for (Overlay overlay : mOverlays) {
+ overlay.dispose();
+ }
+ mOverlays = null;
+ }
+ mCurrentGesture = null;
+ mZombieGesture = null;
+ mLastStateMask = 0;
+ updateMessage(null);
+ updateCursor(mousePos);
+ mCanvas.redraw();
+ }
+ }
+
+ /**
+ * Update the cursor to show the type of operation we expect on a mouse press:
+ * <ul>
+ * <li>Over a selection handle, show a directional cursor depending on the position of
+ * the selection handle
+ * <li>Over a widget, show a move (hand) cursor
+ * <li>Otherwise, show the default arrow cursor
+ * </ul>
+ */
+ void updateCursor(ControlPoint controlPoint) {
+ // We don't hover on the root since it's not a widget per see and it is always there.
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ if (!selectionManager.isEmpty()) {
+ Display display = mCanvas.getDisplay();
+ Pair<SelectionItem, SelectionHandle> handlePair =
+ selectionManager.findHandle(controlPoint);
+ if (handlePair != null) {
+ SelectionHandle handle = handlePair.getSecond();
+ int cursorType = handle.getSwtCursorType();
+ Cursor cursor = display.getSystemCursor(cursorType);
+ if (cursor != mCanvas.getCursor()) {
+ mCanvas.setCursor(cursor);
+ }
+ return;
+ }
+
+ // See if it's over a selected view
+ LayoutPoint layoutPoint = controlPoint.toLayout();
+ for (SelectionItem item : selectionManager.getSelections()) {
+ if (item.getRect().contains(layoutPoint.x, layoutPoint.y)
+ && !item.isRoot()) {
+ Cursor cursor = display.getSystemCursor(SWT.CURSOR_HAND);
+ if (cursor != mCanvas.getCursor()) {
+ mCanvas.setCursor(cursor);
+ }
+ return;
+ }
+ }
+ }
+
+ if (mCanvas.getCursor() != null) {
+ mCanvas.setCursor(null);
+ }
+ }
+
+ /**
+ * Update the Eclipse status message with any feedback messages from the given
+ * {@link DropFeedback} object, or clean up if there is no more feedback to process
+ * @param feedback the feedback whose message we want to display, or null to clear the
+ * message if previously set
+ */
+ void updateMessage(DropFeedback feedback) {
+ IEditorSite editorSite = mCanvas.getEditorDelegate().getEditor().getEditorSite();
+ IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
+ if (feedback == null) {
+ if (mDisplayingMessage != null) {
+ status.setMessage(null);
+ status.setErrorMessage(null);
+ mDisplayingMessage = null;
+ }
+ } else if (feedback.errorMessage != null) {
+ if (!feedback.errorMessage.equals(mDisplayingMessage)) {
+ mDisplayingMessage = feedback.errorMessage;
+ status.setErrorMessage(mDisplayingMessage);
+ }
+ } else if (feedback.message != null) {
+ if (!feedback.message.equals(mDisplayingMessage)) {
+ mDisplayingMessage = feedback.message;
+ status.setMessage(mDisplayingMessage);
+ }
+ } else if (mDisplayingMessage != null) {
+ // TODO: Can we check the existing message and only clear it if it's the
+ // same as the one we set?
+ mDisplayingMessage = null;
+ status.setMessage(null);
+ status.setErrorMessage(null);
+ }
+
+ // Tooltip
+ if (feedback != null && feedback.tooltip != null) {
+ Pair<Boolean,Boolean> position = mCurrentGesture.getTooltipPosition();
+ boolean below = position.getFirst();
+ if (feedback.tooltipY != null) {
+ below = feedback.tooltipY == SegmentType.BOTTOM;
+ }
+ boolean toRightOf = position.getSecond();
+ if (feedback.tooltipX != null) {
+ toRightOf = feedback.tooltipX == SegmentType.RIGHT;
+ }
+ if (mTooltip == null) {
+ mTooltip = new GestureToolTip(mCanvas, below, toRightOf);
+ }
+ mTooltip.update(feedback.tooltip, below, toRightOf);
+ } else if (mTooltip != null) {
+ mTooltip.dispose();
+ mTooltip = null;
+ }
+ }
+
+ /**
+ * Returns the current mouse position as a {@link ControlPoint}
+ *
+ * @return the current mouse position as a {@link ControlPoint}
+ */
+ public ControlPoint getCurrentControlPoint() {
+ return ControlPoint.create(mCanvas, mLastMouseX, mLastMouseY);
+ }
+
+ /**
+ * Returns the current SWT modifier key mask as an {@link IViewRule} modifier mask
+ *
+ * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask
+ */
+ public int getRuleModifierMask() {
+ int swtMask = mLastStateMask;
+ int modifierMask = 0;
+ if ((swtMask & SWT.MOD1) != 0) {
+ modifierMask |= DropFeedback.MODIFIER1;
+ }
+ if ((swtMask & SWT.MOD2) != 0) {
+ modifierMask |= DropFeedback.MODIFIER2;
+ }
+ if ((swtMask & SWT.MOD3) != 0) {
+ modifierMask |= DropFeedback.MODIFIER3;
+ }
+ return modifierMask;
+ }
+
+ /**
+ * Helper class which implements the {@link MouseMoveListener},
+ * {@link MouseListener} and {@link KeyListener} interfaces.
+ */
+ private class Listener implements MouseMoveListener, MouseListener, MouseTrackListener,
+ KeyListener {
+
+ // --- MouseMoveListener ---
+
+ @Override
+ public void mouseMove(MouseEvent e) {
+ mLastMouseX = e.x;
+ mLastMouseY = e.y;
+ mLastStateMask = e.stateMask;
+
+ ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
+ if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+ if (mCurrentGesture != null) {
+ updateMouse(controlPoint, e);
+ mCanvas.redraw();
+ }
+ } else {
+ updateCursor(controlPoint);
+ mCanvas.hover(e);
+ mCanvas.getPreviewManager().moved(controlPoint);
+ }
+ }
+
+ // --- MouseListener ---
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ ControlPoint mousePos = ControlPoint.create(mCanvas, e);
+
+ if (mCurrentGesture == null) {
+ // If clicking on a configuration preview, just process it there
+ if (mCanvas.getPreviewManager().click(mousePos)) {
+ return;
+ }
+
+ // Just a click, select
+ Pair<SelectionItem, SelectionHandle> handlePair =
+ mCanvas.getSelectionManager().findHandle(mousePos);
+ if (handlePair == null) {
+ mCanvas.getSelectionManager().select(e);
+ }
+ }
+ if (mCurrentGesture == null) {
+ updateCursor(mousePos);
+ } else if (mCurrentGesture instanceof DropGesture) {
+ // Mouse Up shouldn't be delivered in the middle of a drag & drop -
+ // but this can happen on some versions of Linux
+ // (see http://code.google.com/p/android/issues/detail?id=19057 )
+ // and if we process the mouseUp it will abort the remainder of
+ // the drag & drop operation, so ignore this event!
+ } else {
+ finishGesture(mousePos, false);
+ }
+ mCanvas.redraw();
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ mLastMouseX = e.x;
+ mLastMouseY = e.y;
+ mLastStateMask = e.stateMask;
+
+ // Not yet used. Should be, for Mac and Linux.
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ // SWT delivers a double click event even if you click two different buttons
+ // in rapid succession. In any case, we only want to let you double click the
+ // first button to warp to XML:
+ if (e.button == 1) {
+ // Warp to the text editor and show the corresponding XML for the
+ // double-clicked widget
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null) {
+ mCanvas.show(vi);
+ }
+ }
+ }
+
+ // --- MouseTrackListener ---
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ ControlPoint mousePos = ControlPoint.create(mCanvas, e);
+ mCanvas.getPreviewManager().enter(mousePos);
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ ControlPoint mousePos = ControlPoint.create(mCanvas, e);
+ mCanvas.getPreviewManager().exit(mousePos);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+
+ // --- KeyListener ---
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ mLastStateMask = e.stateMask;
+ // Workaround for the fact that in keyPressed the current state
+ // mask is not yet updated
+ if (e.keyCode == SWT.SHIFT) {
+ mLastStateMask |= SWT.MOD2;
+ }
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ if (e.keyCode == SWT.COMMAND) {
+ mLastStateMask |= SWT.MOD1;
+ }
+ } else {
+ if (e.keyCode == SWT.CTRL) {
+ mLastStateMask |= SWT.MOD1;
+ }
+ }
+
+ // Give gestures a first chance to see and consume the key press
+ if (mCurrentGesture != null) {
+ // unless it's "Escape", which cancels the gesture
+ if (e.keyCode == SWT.ESC) {
+ ControlPoint controlPoint = ControlPoint.create(mCanvas,
+ mLastMouseX, mLastMouseY);
+ finishGesture(controlPoint, true);
+ return;
+ }
+
+ if (mCurrentGesture.keyPressed(e)) {
+ return;
+ }
+ }
+
+ // Fall back to canvas actions for the key press
+ mCanvas.handleKeyPressed(e);
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ mLastStateMask = e.stateMask;
+ // Workaround for the fact that in keyPressed the current state
+ // mask is not yet updated
+ if (e.keyCode == SWT.SHIFT) {
+ mLastStateMask &= ~SWT.MOD2;
+ }
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ if (e.keyCode == SWT.COMMAND) {
+ mLastStateMask &= ~SWT.MOD1;
+ }
+ } else {
+ if (e.keyCode == SWT.CTRL) {
+ mLastStateMask &= ~SWT.MOD1;
+ }
+ }
+
+ if (mCurrentGesture != null) {
+ mCurrentGesture.keyReleased(e);
+ }
+ }
+ }
+
+ /** Listener for Drag &amp; Drop events. */
+ private class CanvasDropListener implements DropTargetListener {
+ public CanvasDropListener() {
+ }
+
+ /**
+ * The cursor has entered the drop target boundaries. {@inheritDoc}
+ */
+ @Override
+ public void dragEnter(DropTargetEvent event) {
+ mCanvas.showInvisibleViews(true);
+ mCanvas.getEditorDelegate().getGraphicalEditor().dismissHoverPalette();
+
+ if (mCurrentGesture == null) {
+ Gesture newGesture = mZombieGesture;
+ if (newGesture == null) {
+ newGesture = new MoveGesture(mCanvas);
+ } else {
+ mZombieGesture = null;
+ }
+ startGesture(ControlPoint.create(mCanvas, event),
+ newGesture, 0);
+ }
+
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragEnter(event);
+ }
+ }
+
+ /**
+ * The cursor is moving over the drop target. {@inheritDoc}
+ */
+ @Override
+ public void dragOver(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragOver(event);
+ }
+ }
+
+ /**
+ * The cursor has left the drop target boundaries OR data is about to be
+ * dropped. {@inheritDoc}
+ */
+ @Override
+ public void dragLeave(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ DropGesture dropGesture = (DropGesture) mCurrentGesture;
+ dropGesture.dragLeave(event);
+ finishGesture(ControlPoint.create(mCanvas, event), true);
+ mZombieGesture = dropGesture;
+ }
+
+ mCanvas.showInvisibleViews(false);
+ }
+
+ /**
+ * The drop is about to be performed. The drop target is given a last
+ * chance to change the nature of the drop. {@inheritDoc}
+ */
+ @Override
+ public void dropAccept(DropTargetEvent event) {
+ Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+ if (gesture instanceof DropGesture) {
+ ((DropGesture) gesture).dropAccept(event);
+ }
+ }
+
+ /**
+ * The data is being dropped. {@inheritDoc}
+ */
+ @Override
+ public void drop(final DropTargetEvent event) {
+ // See if we had a gesture just prior to the drop (we receive a dragLeave
+ // right before the drop which we don't know whether means the cursor has
+ // left the canvas for good or just before a drop)
+ Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+ mZombieGesture = null;
+
+ if (gesture instanceof DropGesture) {
+ ((DropGesture) gesture).drop(event);
+
+ finishGesture(ControlPoint.create(mCanvas, event), true);
+ }
+ }
+
+ /**
+ * The operation being performed has changed (e.g. modifier key).
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragOperationChanged(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragOperationChanged(event);
+ }
+ }
+ }
+
+ /**
+ * Our canvas {@link DragSourceListener}. Handles drag being started and
+ * finished and generating the drag data.
+ */
+ private class CanvasDragSourceListener implements DragSourceListener {
+
+ /**
+ * The current selection being dragged. This may be a subset of the
+ * canvas selection due to the "sanitize" pass. Can be empty but never
+ * null.
+ */
+ private final ArrayList<SelectionItem> mDragSelection = new ArrayList<SelectionItem>();
+
+ private SimpleElement[] mDragElements;
+
+ /**
+ * The user has begun the actions required to drag the widget.
+ * <p/>
+ * Initiate a drag only if there is one or more item selected. If
+ * there's none, try to auto-select the one under the cursor.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ LayoutPoint p = LayoutPoint.create(mCanvas, e);
+ ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ // See if the mouse is over a selection handle; if so, start a resizing
+ // gesture.
+ Pair<SelectionItem, SelectionHandle> handle =
+ selectionManager.findHandle(controlPoint);
+ if (handle != null) {
+ startGesture(controlPoint, new ResizeGesture(mCanvas, handle.getFirst(),
+ handle.getSecond()), mLastStateMask);
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ mCanvas.redraw();
+ return;
+ }
+
+ // We need a selection (simple or multiple) to do any transfer.
+ // If there's a selection *and* the cursor is over this selection,
+ // use all the currently selected elements.
+ // If there is no selection or the cursor is not over a selected
+ // element, *change* the selection to match the element under the
+ // cursor and use that. If nothing can be selected, abort the drag
+ // operation.
+ List<SelectionItem> selections = selectionManager.getSelections();
+ mDragSelection.clear();
+ SelectionItem primary = null;
+
+ if (!selections.isEmpty()) {
+ // Is the cursor on top of a selected element?
+ boolean insideSelection = false;
+
+ for (SelectionItem cs : selections) {
+ if (!cs.isRoot() && cs.getRect().contains(p.x, p.y)) {
+ primary = cs;
+ insideSelection = true;
+ break;
+ }
+ }
+
+ if (!insideSelection) {
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null && !vi.isRoot() && !vi.isHidden()) {
+ primary = selectionManager.selectSingle(vi);
+ insideSelection = true;
+ }
+ }
+
+ if (insideSelection) {
+ // We should now have a proper selection that matches the
+ // cursor. Let's use this one. We make a copy of it since
+ // the "sanitize" pass below might remove some of the
+ // selected objects.
+ if (selections.size() == 1) {
+ // You are dragging just one element - this might or
+ // might not be the root, but if it's the root that is
+ // fine since we will let you drag the root if it is the
+ // only thing you are dragging.
+ mDragSelection.addAll(selections);
+ } else {
+ // Only drag non-root items.
+ for (SelectionItem cs : selections) {
+ if (!cs.isRoot() && !cs.isHidden()) {
+ mDragSelection.add(cs);
+ } else if (cs == primary) {
+ primary = null;
+ }
+ }
+ }
+ }
+ }
+
+ // If you are dragging a non-selected item, select it
+ if (mDragSelection.isEmpty()) {
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null && !vi.isRoot() && !vi.isHidden()) {
+ primary = selectionManager.selectSingle(vi);
+ mDragSelection.addAll(selections);
+ }
+ }
+
+ SelectionManager.sanitize(mDragSelection);
+
+ e.doit = !mDragSelection.isEmpty();
+ int imageCount = mDragSelection.size();
+ if (e.doit) {
+ mDragElements = SelectionItem.getAsElements(mDragSelection, primary);
+ GlobalCanvasDragInfo.getInstance().startDrag(mDragElements,
+ mDragSelection.toArray(new SelectionItem[imageCount]),
+ mCanvas, new Runnable() {
+ @Override
+ public void run() {
+ mCanvas.getClipboardSupport().deleteSelection("Remove",
+ mDragSelection);
+ }
+ });
+ }
+
+ // If you drag on the -background-, we make that into a marquee
+ // selection
+ if (!e.doit || (imageCount == 1
+ && (mDragSelection.get(0).isRoot() || mDragSelection.get(0).isHidden()))) {
+ boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0;
+ startGesture(controlPoint,
+ new MarqueeGesture(mCanvas, toggle), mLastStateMask);
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ } else {
+ // Otherwise, the drag means you are moving something
+ mCanvas.showInvisibleViews(true);
+ startGesture(controlPoint, new MoveGesture(mCanvas), 0);
+
+ // Render drag-images: Copy portions of the full screen render.
+ Image image = mCanvas.getImageOverlay().getImage();
+ if (image != null) {
+ /**
+ * Transparency of the dragged image ([0-255]). We're using 30%
+ * translucency to make the image faint and not obscure the drag
+ * feedback below it.
+ */
+ final byte DRAG_TRANSPARENCY = (byte) (0.3 * 255);
+
+ List<Rectangle> rectangles = new ArrayList<Rectangle>(imageCount);
+ if (imageCount > 0) {
+ ImageData data = image.getImageData();
+ Rectangle imageRectangle = new Rectangle(0, 0, data.width, data.height);
+ for (SelectionItem item : mDragSelection) {
+ Rectangle bounds = item.getRect();
+ // Some bounds can be outside the rendered rectangle (for
+ // example, in an absolute layout, you can have negative
+ // coordinates), so create the intersection of these bounds.
+ Rectangle clippedBounds = imageRectangle.intersection(bounds);
+ rectangles.add(clippedBounds);
+ }
+ Rectangle boundingBox = ImageUtils.getBoundingRectangle(rectangles);
+ double scale = mCanvas.getHorizontalTransform().getScale();
+ e.image = SwtUtils.drawRectangles(image, rectangles, boundingBox, scale,
+ DRAG_TRANSPARENCY);
+
+ // Set the image offset such that we preserve the relative
+ // distance between the mouse pointer and the top left corner of
+ // the dragged view
+ int deltaX = (int) (scale * (boundingBox.x - p.x));
+ int deltaY = (int) (scale * (boundingBox.y - p.y));
+ e.offsetX = -deltaX;
+ e.offsetY = -deltaY;
+
+ // View rules may need to know it as well
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ Rect dragBounds = null;
+ int width = (int) (scale * boundingBox.width);
+ int height = (int) (scale * boundingBox.height);
+ dragBounds = new Rect(deltaX, deltaY, width, height);
+ dragInfo.setDragBounds(dragBounds);
+
+ // Record the baseline such that we can perform baseline alignment
+ // on the node as it's dragged around
+ NodeProxy firstNode =
+ mCanvas.getNodeFactory().create(mDragSelection.get(0).getViewInfo());
+ dragInfo.setDragBaseline(firstNode.getBaseline());
+ }
+ }
+ }
+
+ // No hover during drag (since no mouse over events are delivered
+ // during a drag to keep the hovers up to date anyway)
+ mCanvas.clearHover();
+
+ mCanvas.redraw();
+ }
+
+ /**
+ * Callback invoked when data is needed for the event, typically right
+ * before drop. The drop side decides what type of transfer to use and
+ * this side must now provide the adequate data. {@inheritDoc}
+ */
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = SelectionItem.getAsText(mCanvas, mDragSelection);
+ return;
+ }
+
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mDragElements;
+ return;
+ }
+
+ // otherwise we failed
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ }
+
+ /**
+ * Callback invoked when the drop has been finished either way. On a
+ * successful move, remove the originating elements.
+ */
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Clear the selection
+ mDragSelection.clear();
+ mDragElements = null;
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+
+ finishGesture(ControlPoint.create(mCanvas, e), e.detail == DND.DROP_NONE);
+ mCanvas.showInvisibleViews(false);
+ mCanvas.redraw();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java
new file mode 100644
index 000000000..a49e79cbf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * A dedicated tooltip used during gestures, for example to show the resize dimensions.
+ * <p>
+ * This is necessary because {@link org.eclipse.jface.window.ToolTip} causes flicker when
+ * used to dynamically update the position and text of the tip, and it does not seem to
+ * have setter methods to update the text or position without recreating the tip.
+ */
+public class GestureToolTip {
+ /** Minimum number of milliseconds to wait between alignment changes */
+ private static final int TIMEOUT_MS = 750;
+
+ /**
+ * The alpha to use for the tooltip window (which sadly will apply to the tooltip text
+ * as well.)
+ */
+ private static final int SHELL_TRANSPARENCY = 220;
+
+ /** The size of the font displayed in the tooltip */
+ private static final int FONT_SIZE = 9;
+
+ /** Horizontal delta from the mouse cursor to shift the tooltip by */
+ private static final int OFFSET_X = 20;
+
+ /** Vertical delta from the mouse cursor to shift the tooltip by */
+ private static final int OFFSET_Y = 20;
+
+ /** The label which displays the tooltip */
+ private CLabel mLabel;
+
+ /** The shell holding the tooltip */
+ private Shell mShell;
+
+ /** The font shown in the label; held here such that it can be disposed of after use */
+ private Font mFont;
+
+ /** Is the tooltip positioned below the given anchor? */
+ private boolean mBelow;
+
+ /** Is the tooltip positioned to the right of the given anchor? */
+ private boolean mToRightOf;
+
+ /** Is an alignment change pending? */
+ private boolean mTimerPending;
+
+ /** The new value for {@link #mBelow} when the timer expires */
+ private boolean mPendingBelow;
+
+ /** The new value for {@link #mToRightOf} when the timer expires */
+ private boolean mPendingRight;
+
+ /** The time stamp (from {@link System#currentTimeMillis()} of the last alignment change */
+ private long mLastAlignmentTime;
+
+ /**
+ * Creates a new tooltip over the given parent with the given relative position.
+ *
+ * @param parent the parent control
+ * @param below if true, display the tooltip below the mouse cursor otherwise above
+ * @param toRightOf if true, display the tooltip to the right of the mouse cursor,
+ * otherwise to the left
+ */
+ public GestureToolTip(Composite parent, boolean below, boolean toRightOf) {
+ mBelow = below;
+ mToRightOf = toRightOf;
+ mLastAlignmentTime = System.currentTimeMillis();
+
+ mShell = new Shell(parent.getShell(), SWT.ON_TOP | SWT.TOOL | SWT.NO_FOCUS);
+ mShell.setLayout(new FillLayout());
+ mShell.setAlpha(SHELL_TRANSPARENCY);
+
+ Display display = parent.getDisplay();
+ mLabel = new CLabel(mShell, SWT.SHADOW_NONE);
+ mLabel.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ mLabel.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+
+ Font systemFont = display.getSystemFont();
+ FontData[] fd = systemFont.getFontData();
+ for (int i = 0; i < fd.length; i++) {
+ fd[i].setHeight(FONT_SIZE);
+ }
+ mFont = new Font(display, fd);
+ mLabel.setFont(mFont);
+
+ mShell.setVisible(false);
+ }
+
+ /**
+ * Show the tooltip at the given position and with the given text. Note that the
+ * position may not be applied immediately; to prevent flicker alignment changes
+ * are queued up with a timer (unless it's been a while since the last change, in
+ * which case the update is applied immediately.)
+ *
+ * @param text the new text to be displayed
+ * @param below if true, display the tooltip below the mouse cursor otherwise above
+ * @param toRightOf if true, display the tooltip to the right of the mouse cursor,
+ * otherwise to the left
+ */
+ public void update(final String text, boolean below, boolean toRightOf) {
+ // If the alignment has not changed recently, just apply the change immediately
+ // instead of within a delay
+ if (!mTimerPending && (below != mBelow || toRightOf != mToRightOf)
+ && (System.currentTimeMillis() - mLastAlignmentTime >= TIMEOUT_MS)) {
+ mBelow = below;
+ mToRightOf = toRightOf;
+ mLastAlignmentTime = System.currentTimeMillis();
+ }
+
+ Point location = mShell.getDisplay().getCursorLocation();
+
+ mLabel.setText(text);
+
+ // Pack the label to its minimum size -- unless we are positioning the tooltip
+ // on the left. Because of the way SWT works (at least on the OSX) this sometimes
+ // creates flicker, because when we switch to a longer string (such as when
+ // switching from "52dp" to "wrap_content" during a resize) the window size will
+ // change first, and then the location will update later - so there will be a
+ // brief flash of the longer label before it is moved to the right position on the
+ // left. To work around this, we simply pass false to pack such that it will reuse
+ // its cached size, which in practice means that for labels on the right, the
+ // label will grow but not shrink.
+ // This workaround is disabled because it doesn't work well in Eclipse 3.5; the
+ // labels don't grow when they should. Re-enable when we drop 3.5 support.
+ //boolean changed = mToRightOf;
+ boolean changed = true;
+
+ mShell.pack(changed);
+ Point size = mShell.getSize();
+
+ // Position the tooltip to the left or right, and above or below, according
+ // to the saved state of these flags, not the current parameters. We don't want
+ // to flicker, instead we react on a timer to changes in alignment below.
+ if (mBelow) {
+ location.y += OFFSET_Y;
+ } else {
+ location.y -= OFFSET_Y;
+ location.y -= size.y;
+ }
+
+ if (mToRightOf) {
+ location.x += OFFSET_X;
+ } else {
+ location.x -= OFFSET_X;
+ location.x -= size.x;
+ }
+
+ mShell.setLocation(location);
+
+ if (!mShell.isVisible()) {
+ mShell.setVisible(true);
+ }
+
+ // Has the orientation changed?
+ mPendingBelow = below;
+ mPendingRight = toRightOf;
+ if (below != mBelow || toRightOf != mToRightOf) {
+ // Yes, so schedule a timer (unless one is already scheduled)
+ if (!mTimerPending) {
+ mTimerPending = true;
+ final Runnable timer = new Runnable() {
+ @Override
+ public void run() {
+ mTimerPending = false;
+ // Check whether the alignment is still different than the target
+ // (since we may change back and forth repeatedly during the timeout)
+ if (mBelow != mPendingBelow || mToRightOf != mPendingRight) {
+ mBelow = mPendingBelow;
+ mToRightOf = mPendingRight;
+ mLastAlignmentTime = System.currentTimeMillis();
+ if (mShell != null && mShell.isVisible()) {
+ update(text, mBelow, mToRightOf);
+ }
+ }
+ }
+ };
+ mShell.getDisplay().timerExec(TIMEOUT_MS, timer);
+ }
+ }
+ }
+
+ /** Hide the tooltip and dispose of any associated resources */
+ public void dispose() {
+ mShell.dispose();
+ mFont.dispose();
+
+ mShell = null;
+ mFont = null;
+ mLabel = null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java
new file mode 100644
index 000000000..b918b00bf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Rect;
+
+
+/**
+ * This singleton is used to keep track of drag'n'drops initiated within this
+ * session of Eclipse. A drag can be initiated from a palette or from a canvas
+ * and its content is an Android View fully-qualified class name.
+ * <p/>
+ * Overall this is a workaround: the issue is that the drag'n'drop SWT API does not
+ * allow us to know the transfered data during the initial drag -- only when the
+ * data is dropped do we know what it is about (and to be more exact there is a workaround
+ * to do just that which works on Windows but not on Linux/Mac SWT).
+ * <p/>
+ * In the GLE we'd like to adjust drag feedback to the data being actually dropped.
+ * The singleton instance of this class will be used to track the data currently dragged
+ * off a canvas or its palette and then set back to null when the drag'n'drop is finished.
+ * <p/>
+ * Note that when a drag starts in one instance of Eclipse and the dragOver/drop is done
+ * in a <em>separate</em> instance of Eclipse, the dragged FQCN won't be registered here
+ * and will be null.
+ */
+final class GlobalCanvasDragInfo {
+
+ private static final GlobalCanvasDragInfo sInstance = new GlobalCanvasDragInfo();
+
+ private SimpleElement[] mCurrentElements = null;
+ private SelectionItem[] mCurrentSelection;
+ private Object mSourceCanvas = null;
+ private Runnable mRemoveSourceHandler;
+ private Rect mDragBounds;
+ private int mDragBaseline = -1;
+
+ /** Private constructor. Use {@link #getInstance()} to retrieve the singleton. */
+ private GlobalCanvasDragInfo() {
+ // pass
+ }
+
+ /** Returns the singleton instance. */
+ public static GlobalCanvasDragInfo getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Registers the XML elements being dragged.
+ *
+ * @param elements The elements being dragged
+ * @param primary the "primary" element among the elements; when there is a
+ * single item dragged this will be the same, but in
+ * multi-selection it will be the element under the mouse as the
+ * selection was initiated
+ * @param selection The selection (which can be null, for example when the
+ * user drags from the palette)
+ * @param sourceCanvas An object representing the source we are dragging
+ * from (used for identity comparisons only)
+ * @param removeSourceHandler A runnable (or null) which can clean up the
+ * source. It should only be invoked if the drag operation is a
+ * move, not a copy.
+ */
+ public void startDrag(
+ @NonNull SimpleElement[] elements,
+ @Nullable SelectionItem[] selection,
+ @Nullable Object sourceCanvas,
+ @Nullable Runnable removeSourceHandler) {
+ mCurrentElements = elements;
+ mCurrentSelection = selection;
+ mSourceCanvas = sourceCanvas;
+ mRemoveSourceHandler = removeSourceHandler;
+ }
+
+ /** Unregisters elements being dragged. */
+ public void stopDrag() {
+ mCurrentElements = null;
+ mCurrentSelection = null;
+ mSourceCanvas = null;
+ mRemoveSourceHandler = null;
+ mDragBounds = null;
+ }
+
+ public boolean isDragging() {
+ return mCurrentElements != null;
+ }
+
+ /** Returns the elements being dragged. */
+ @NonNull
+ public SimpleElement[] getCurrentElements() {
+ return mCurrentElements;
+ }
+
+ /** Returns the selection originally dragged.
+ * Can be null if the drag did not start in a canvas.
+ */
+ public SelectionItem[] getCurrentSelection() {
+ return mCurrentSelection;
+ }
+
+ /**
+ * Returns the object that call {@link #startDrag(SimpleElement[], SelectionItem[], Object)}.
+ * Can be null.
+ * This is not meant to access the object indirectly, it is just meant to compare if the
+ * source and the destination of the drag'n'drop are the same, so object identity
+ * is all what matters.
+ */
+ public Object getSourceCanvas() {
+ return mSourceCanvas;
+ }
+
+ /**
+ * Removes source of the drag. This should only be called when the drag and
+ * drop operation is a move (not a copy).
+ */
+ public void removeSource() {
+ if (mRemoveSourceHandler != null) {
+ mRemoveSourceHandler.run();
+ mRemoveSourceHandler = null;
+ }
+ }
+
+ /**
+ * Get the bounds of the drag, relative to the starting mouse position. For example,
+ * if you have a rectangular view of size 100x80, and you start dragging at position
+ * (15,20) from the top left corner of this rectangle, then the drag bounds would be
+ * (-15,-20, 100x80).
+ * <p>
+ * NOTE: The coordinate units will be in SWT/control pixels, not Android view pixels.
+ * In other words, they are affected by the canvas zoom: If you zoom the view and the
+ * bounds of a view grow, the drag bounds will be larger.
+ *
+ * @return the drag bounds, or null if there are no bounds for the current drag
+ */
+ public Rect getDragBounds() {
+ return mDragBounds;
+ }
+
+ /**
+ * Set the bounds of the drag, relative to the starting mouse position. See
+ * {@link #getDragBounds()} for details on the semantics of the drag bounds.
+ *
+ * @param dragBounds the new drag bounds, or null if there are no drag bounds
+ */
+ public void setDragBounds(Rect dragBounds) {
+ mDragBounds = dragBounds;
+ }
+
+ /**
+ * Returns the baseline of the drag, or -1 if not applicable
+ *
+ * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask
+ */
+ public int getDragBaseline() {
+ return mDragBaseline;
+ }
+
+ /**
+ * Sets the baseline of the drag
+ *
+ * @param baseline the new baseline
+ */
+ public void setDragBaseline(int baseline) {
+ mDragBaseline = baseline;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
new file mode 100644
index 000000000..0f5762da6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
@@ -0,0 +1,2937 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_PKG;
+import static com.android.SdkConstants.ANDROID_STRING_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.FD_GEN_SOURCES;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.SCROLL_VIEW;
+import static com.android.SdkConstants.STRING_PREFIX;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.ide.common.rendering.RenderSecurityManager.ENABLED_PROPERTY;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET;
+import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_EAST;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_WEST;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_COLLAPSED;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_OPEN;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.RenderSecurityException;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.StaticRenderSession;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationMatcher;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PaletteControl.PalettePage;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.resources.Density;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModelMarker;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.ui.preferences.BuildPathsPropertyPage;
+import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
+import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.text.edits.MalformedTreeException;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.INullSelectionListener;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.PreferencesUtil;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.EditorPart;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.IPageSite;
+import org.eclipse.ui.part.PageBookView;
+import org.eclipse.wb.core.controls.flyout.FlyoutControlComposite;
+import org.eclipse.wb.core.controls.flyout.IFlyoutListener;
+import org.eclipse.wb.core.controls.flyout.PluginFlyoutPreferences;
+import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Graphical layout editor part, version 2.
+ * <p/>
+ * The main component of the editor part is the {@link LayoutCanvasViewer}, which
+ * actually delegates its work to the {@link LayoutCanvas} control.
+ * <p/>
+ * The {@link LayoutCanvasViewer} is set as the site's {@link ISelectionProvider}:
+ * when the selection changes in the canvas, it is thus broadcasted to anyone listening
+ * on the site's selection service.
+ * <p/>
+ * This part is also an {@link ISelectionListener}. It listens to the site's selection
+ * service and thus receives selection changes from itself as well as the associated
+ * outline and property sheet (these are registered by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}).
+ *
+ * @since GLE2
+ */
+public class GraphicalEditorPart extends EditorPart
+ implements IPageImageProvider, INullSelectionListener, IFlyoutListener,
+ ConfigurationClient {
+
+ /*
+ * Useful notes:
+ * To understand Drag & drop:
+ * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
+ *
+ * To understand the site's selection listener, selection provider, and the
+ * confusion of different-yet-similarly-named interfaces, consult this:
+ * http://www.eclipse.org/articles/Article-WorkbenchSelections/article.html
+ *
+ * To summarize the selection mechanism:
+ * - The workbench site selection service can be seen as "centralized"
+ * service that registers selection providers and selection listeners.
+ * - The editor part and the outline are selection providers.
+ * - The editor part, the outline and the property sheet are listeners
+ * which all listen to each others indirectly.
+ */
+
+ /** Property key for the window preferences for the structure flyout */
+ private static final String PREF_STRUCTURE = "design.structure"; //$NON-NLS-1$
+
+ /** Property key for the window preferences for the palette flyout */
+ private static final String PREF_PALETTE = "design.palette"; //$NON-NLS-1$
+
+ /**
+ * Session-property on files which specifies the initial config state to be used on
+ * this file
+ */
+ public final static QualifiedName NAME_INITIAL_STATE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "initialstate");//$NON-NLS-1$
+
+ /**
+ * Session-property on files which specifies the inclusion-context (reference to another layout
+ * which should be "including" this layout) when the file is opened
+ */
+ public final static QualifiedName NAME_INCLUDE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "includer");//$NON-NLS-1$
+
+ /** Reference to the layout editor */
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
+ private IFile mEditedFile;
+
+ /** The configuration chooser at the top of the layout editor. */
+ private ConfigurationChooser mConfigChooser;
+
+ /** The sash that splits the palette from the error view.
+ * The error view is shown only when needed. */
+ private SashForm mSashError;
+
+ /** The palette displayed on the left of the sash. */
+ private PaletteControl mPalette;
+
+ /** The layout canvas displayed to the right of the sash. */
+ private LayoutCanvasViewer mCanvasViewer;
+
+ /** The Rules Engine associated with this editor. It is project-specific. */
+ private RulesEngine mRulesEngine;
+
+ /** Styled text displaying the most recent error in the error view. */
+ private StyledText mErrorLabel;
+
+ /**
+ * The resource reference to a file that should surround this file (e.g. include this file
+ * visually), or null if not applicable
+ */
+ private Reference mIncludedWithin;
+
+ private Map<ResourceType, Map<String, ResourceValue>> mConfiguredFrameworkRes;
+ private Map<ResourceType, Map<String, ResourceValue>> mConfiguredProjectRes;
+ private ProjectCallback mProjectCallback;
+ private boolean mNeedsRecompute = false;
+ private TargetListener mTargetListener;
+ private ResourceResolver mResourceResolver;
+ private ReloadListener mReloadListener;
+ private int mMinSdkVersion;
+ private int mTargetSdkVersion;
+ private LayoutActionBar mActionBar;
+ private OutlinePage mOutlinePage;
+ private FlyoutControlComposite mStructureFlyout;
+ private FlyoutControlComposite mPaletteComposite;
+ private PropertyFactory mPropertyFactory;
+ private boolean mRenderedOnce;
+ private final Object mCredential = new Object();
+
+ /**
+ * Flags which tracks whether this editor is currently active which is set whenever
+ * {@link #activated()} is called and clear whenever {@link #deactivated()} is called.
+ * This is used to suppress repeated calls to {@link #activate()} to avoid doing
+ * unnecessary work.
+ */
+ private boolean mActive;
+
+ /**
+ * Constructs a new {@link GraphicalEditorPart}
+ *
+ * @param editorDelegate the associated XML editor delegate
+ */
+ public GraphicalEditorPart(@NonNull LayoutEditorDelegate editorDelegate) {
+ mEditorDelegate = editorDelegate;
+ setPartName("Graphical Layout");
+ }
+
+ // ------------------------------------
+ // Methods overridden from base classes
+ //------------------------------------
+
+ /**
+ * Initializes the editor part with a site and input.
+ * {@inheritDoc}
+ */
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ setSite(site);
+ useNewEditorInput(input);
+
+ if (mTargetListener == null) {
+ mTargetListener = new TargetListener();
+ AdtPlugin.getDefault().addTargetListener(mTargetListener);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+ }
+
+ private void useNewEditorInput(IEditorInput input) throws PartInitException {
+ // The contract of init() mentions we need to fail if we can't understand the input.
+ if (!(input instanceof FileEditorInput)) {
+ throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$
+ input == null ? "null" : input.toString()); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+
+ Display d = parent.getDisplay();
+
+ GridLayout gl = new GridLayout(1, false);
+ parent.setLayout(gl);
+ gl.marginHeight = gl.marginWidth = 0;
+
+ // Check whether somebody has requested an initial state for the newly opened file.
+ // The initial state is a serialized version of the state compatible with
+ // {@link ConfigurationComposite#CONFIG_STATE}.
+ String initialState = null;
+ IFile file = mEditedFile;
+ if (file == null) {
+ IEditorInput input = mEditorDelegate.getEditor().getEditorInput();
+ if (input instanceof FileEditorInput) {
+ file = ((FileEditorInput) input).getFile();
+ }
+ }
+
+ if (file != null) {
+ try {
+ initialState = (String) file.getSessionProperty(NAME_INITIAL_STATE);
+ if (initialState != null) {
+ // Only use once
+ file.setSessionProperty(NAME_INITIAL_STATE, null);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't read session property %1$s", NAME_INITIAL_STATE);
+ }
+ }
+
+ IPreferenceStore preferenceStore = AdtPlugin.getDefault().getPreferenceStore();
+ PluginFlyoutPreferences preferences;
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_PALETTE);
+ preferences.initializeDefaults(DOCK_WEST, STATE_OPEN, 200);
+ mPaletteComposite = new FlyoutControlComposite(parent, SWT.NONE, preferences);
+ mPaletteComposite.setTitleText("Palette");
+ mPaletteComposite.setMinWidth(100);
+ Composite paletteParent = mPaletteComposite.getFlyoutParent();
+ Composite editorParent = mPaletteComposite.getClientParent();
+ mPaletteComposite.setListener(this);
+
+ mPaletteComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ PageSiteComposite paletteComposite = new PageSiteComposite(paletteParent, SWT.BORDER);
+ paletteComposite.setTitleText("Palette");
+ paletteComposite.setTitleImage(IconFactory.getInstance().getIcon("palette"));
+ PalettePage decor = new PalettePage(this);
+ paletteComposite.setPage(decor);
+ mPalette = (PaletteControl) decor.getControl();
+ decor.createToolbarItems(paletteComposite.getToolBar());
+
+ // Create the shared structure+editor area
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_STRUCTURE);
+ preferences.initializeDefaults(DOCK_EAST, STATE_OPEN, 300);
+ mStructureFlyout = new FlyoutControlComposite(editorParent, SWT.NONE, preferences);
+ mStructureFlyout.setTitleText("Structure");
+ mStructureFlyout.setMinWidth(150);
+ mStructureFlyout.setListener(this);
+
+ Composite layoutBarAndCanvas = new Composite(mStructureFlyout.getClientParent(), SWT.NONE);
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.horizontalSpacing = 0;
+ gridLayout.verticalSpacing = 0;
+ gridLayout.marginWidth = 0;
+ gridLayout.marginHeight = 0;
+ layoutBarAndCanvas.setLayout(gridLayout);
+
+ mConfigChooser = new ConfigurationChooser(this, layoutBarAndCanvas, initialState);
+ mConfigChooser.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mActionBar = new LayoutActionBar(layoutBarAndCanvas, SWT.NONE, this);
+ GridData detailsData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1);
+ mActionBar.setLayoutData(detailsData);
+ if (file != null) {
+ mActionBar.updateErrorIndicator(file);
+ }
+
+ mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER);
+ mSashError.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mCanvasViewer = new LayoutCanvasViewer(mEditorDelegate, mRulesEngine, mSashError, SWT.NONE);
+ mSashError.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+ mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
+ mErrorLabel.setEditable(false);
+ mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+ mErrorLabel.addMouseListener(new ErrorLabelListener());
+
+ mSashError.setWeights(new int[] { 80, 20 });
+ mSashError.setMaximizedControl(mCanvasViewer.getControl());
+
+ // Create the structure views. We really should do this *lazily*, but that
+ // seems to cause a bug: property sheet won't update. Track this down later.
+ createStructureViews(mStructureFlyout.getFlyoutParent(), false);
+ showStructureViews(false, false, false);
+
+ // Initialize the state
+ reloadPalette();
+
+ IWorkbenchPartSite site = getSite();
+ site.setSelectionProvider(mCanvasViewer);
+ site.getPage().addSelectionListener(this);
+ }
+
+ private void createStructureViews(Composite parent, boolean createPropertySheet) {
+ mOutlinePage = new OutlinePage(this);
+ mOutlinePage.setShowPropertySheet(createPropertySheet);
+ mOutlinePage.setShowHeader(true);
+
+ IPageSite pageSite = new IPageSite() {
+
+ @Override
+ public IWorkbenchPage getPage() {
+ return getSite().getPage();
+ }
+
+ @Override
+ public ISelectionProvider getSelectionProvider() {
+ return getSite().getSelectionProvider();
+ }
+
+ @Override
+ public Shell getShell() {
+ return getSite().getShell();
+ }
+
+ @Override
+ public IWorkbenchWindow getWorkbenchWindow() {
+ return getSite().getWorkbenchWindow();
+ }
+
+ @Override
+ public void setSelectionProvider(ISelectionProvider provider) {
+ getSite().setSelectionProvider(provider);
+ }
+
+ @Override
+ public Object getAdapter(Class adapter) {
+ return getSite().getAdapter(adapter);
+ }
+
+ @Override
+ public Object getService(Class api) {
+ return getSite().getService(api);
+ }
+
+ @Override
+ public boolean hasService(Class api) {
+ return getSite().hasService(api);
+ }
+
+ @Override
+ public void registerContextMenu(String menuId, MenuManager menuManager,
+ ISelectionProvider selectionProvider) {
+ }
+
+ @Override
+ public IActionBars getActionBars() {
+ return null;
+ }
+ };
+ mOutlinePage.init(pageSite);
+ mOutlinePage.createControl(parent);
+ mOutlinePage.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ getCanvasControl().getSelectionManager().setSelection(event.getSelection());
+ }
+ });
+ }
+
+ /** Shows the embedded (within the layout editor) outline and or properties */
+ void showStructureViews(final boolean showOutline, final boolean showProperties,
+ final boolean updateLayout) {
+ Display display = mConfigChooser.getDisplay();
+ if (display.getThread() != Thread.currentThread()) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!mConfigChooser.isDisposed()) {
+ showStructureViews(showOutline, showProperties, updateLayout);
+ }
+ }
+
+ });
+ return;
+ }
+
+ boolean show = showOutline || showProperties;
+
+ Control[] children = mStructureFlyout.getFlyoutParent().getChildren();
+ if (children.length == 0) {
+ if (show) {
+ createStructureViews(mStructureFlyout.getFlyoutParent(), showProperties);
+ }
+ return;
+ }
+
+ mOutlinePage.setShowPropertySheet(showProperties);
+
+ Control control = children[0];
+ if (show != control.getVisible()) {
+ control.setVisible(show);
+ mOutlinePage.setActive(show); // disable/re-enable listeners etc
+ if (show) {
+ ISelection selection = getCanvasControl().getSelectionManager().getSelection();
+ mOutlinePage.selectionChanged(getEditorDelegate().getEditor(), selection);
+ }
+ if (updateLayout) {
+ mStructureFlyout.layout();
+ }
+ // TODO: *dispose* the non-showing widgets to save memory?
+ }
+ }
+
+ /**
+ * Returns the property factory associated with this editor
+ *
+ * @return the factory
+ */
+ @NonNull
+ public PropertyFactory getPropertyFactory() {
+ if (mPropertyFactory == null) {
+ mPropertyFactory = new PropertyFactory(this);
+ }
+
+ return mPropertyFactory;
+ }
+
+ /**
+ * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
+ *
+ * @param rootViewInfo The root of the view info hierarchy. Can be null.
+ */
+ public void setModel(CanvasViewInfo rootViewInfo) {
+ if (mOutlinePage != null) {
+ mOutlinePage.setModel(rootViewInfo);
+ }
+ }
+
+ /**
+ * Listens to workbench selections that does NOT come from {@link LayoutEditorDelegate}
+ * (those are generated by ourselves).
+ * <p/>
+ * Selection can be null, as indicated by this class implementing
+ * {@link INullSelectionListener}.
+ */
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ Object delegate = part instanceof IEditorPart ?
+ LayoutEditorDelegate.fromEditor((IEditorPart) part) : null;
+ if (delegate == null) {
+ if (part instanceof PageBookView) {
+ PageBookView pbv = (PageBookView) part;
+ org.eclipse.ui.part.IPage currentPage = pbv.getCurrentPage();
+ if (currentPage instanceof OutlinePage) {
+ LayoutCanvas canvas = getCanvasControl();
+ if (canvas != null && canvas.getOutlinePage() != currentPage) {
+ // The notification is not for this view; ignore
+ // (can happen when there are multiple pages simultaneously
+ // visible)
+ return;
+ }
+ }
+ }
+ mCanvasViewer.setSelection(selection);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ getSite().getPage().removeSelectionListener(this);
+ getSite().setSelectionProvider(null);
+
+ if (mTargetListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetListener);
+ mTargetListener = null;
+ }
+
+ if (mReloadListener != null) {
+ LayoutReloadMonitor.getMonitor().removeListener(mReloadListener);
+ mReloadListener = null;
+ }
+
+ if (mCanvasViewer != null) {
+ mCanvasViewer.dispose();
+ mCanvasViewer = null;
+ }
+ super.dispose();
+ }
+
+ /**
+ * Select the visual element corresponding to the given XML node
+ * @param xmlNode The Node whose element we want to select
+ */
+ public void select(Node xmlNode) {
+ mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode);
+ }
+
+ // ---- Implements ConfigurationClient ----
+ @Override
+ public void aboutToChange(int flags) {
+ if ((flags & CFG_TARGET) != 0) {
+ IAndroidTarget oldTarget = mConfigChooser.getConfiguration().getTarget();
+ preRenderingTargetChangeCleanUp(oldTarget);
+ }
+ }
+
+ @Override
+ public boolean changed(int flags) {
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+ mResourceResolver = null;
+
+ if (mEditedFile == null) {
+ return true;
+ }
+
+ // Before doing the normal process, test for the following case.
+ // - the editor is being opened (or reset for a new input)
+ // - the file being opened is not the best match for any possible configuration
+ // - another random compatible config was chosen in the config composite.
+ // The result is that 'match' will not be the file being edited, but because this is not
+ // due to a config change, we should not trigger opening the actual best match (also,
+ // because the editor is still opening the MatchingStrategy woudln't answer true
+ // and the best match file would open in a different editor).
+ // So the solution is that if the editor is being created, we just call recomputeLayout
+ // without looking for a better matching layout file.
+ if (mEditorDelegate.getEditor().isCreatingPages()) {
+ recomputeLayout();
+ } else {
+ boolean affectsFileSelection = (flags & Configuration.MASK_FILE_ATTRS) != 0;
+ IFile best = null;
+ // get the resources of the file's project.
+ if (affectsFileSelection) {
+ best = ConfigurationMatcher.getBestFileMatch(mConfigChooser);
+ }
+ if (best != null) {
+ if (!best.equals(mEditedFile)) {
+ try {
+ // tell the editor that the next replacement file is due to a config
+ // change.
+ mEditorDelegate.setNewFileOnConfigChange(true);
+
+ boolean reuseEditor = AdtPrefs.getPrefs().isSharedLayoutEditor();
+ if (!reuseEditor) {
+ String data = ConfigurationDescription.getDescription(best);
+ if (data == null) {
+ // Not previously opened: duplicate the current state as
+ // much as possible
+ data = mConfigChooser.getConfiguration().toPersistentString();
+ ConfigurationDescription.setDescription(best, data);
+ }
+ }
+
+ // ask the IDE to open the replacement file.
+ IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), best,
+ CommonXmlEditor.ID);
+
+ // we're done!
+ return reuseEditor;
+ } catch (PartInitException e) {
+ // FIXME: do something!
+ }
+ }
+
+ // at this point, we have not opened a new file.
+
+ // Store the state in the current file
+ mConfigChooser.saveConstraints();
+
+ // Even though the layout doesn't change, the config changed, and referenced
+ // resources need to be updated.
+ recomputeLayout();
+ } else if (affectsFileSelection) {
+ // display the error.
+ Configuration configuration = mConfigChooser.getConfiguration();
+ FolderConfiguration currentConfig = configuration.getFullConfig();
+ displayError(
+ "No resources match the configuration\n" +
+ " \n" +
+ "\t%1$s\n" +
+ " \n" +
+ "Change the configuration or create:\n" +
+ " \n" +
+ "\tres/%2$s/%3$s\n" +
+ " \n" +
+ "You can also click the 'Create New...' item in the configuration " +
+ "dropdown menu above.",
+ currentConfig.toDisplayString(),
+ currentConfig.getFolderName(ResourceFolderType.LAYOUT),
+ mEditedFile.getName());
+ } else {
+ // Something else changed, such as the theme - just recompute existing
+ // layout
+ mConfigChooser.saveConstraints();
+ recomputeLayout();
+ }
+ }
+
+ if ((flags & CFG_TARGET) != 0) {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ IAndroidTarget target = configuration.getTarget();
+ Sdk current = Sdk.getCurrent();
+ if (current != null) {
+ AndroidTargetData targetData = current.getTargetData(target);
+ updateCapabilities(targetData);
+ }
+ }
+
+ if ((flags & (CFG_DEVICE | CFG_DEVICE_STATE)) != 0) {
+ // When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom
+ // out to fit the content, or zoom back in if we were zoomed out more from the
+ // previous view, but only up to 100% such that we never blow up pixels
+ if (mActionBar.isZoomingAllowed()) {
+ getCanvasControl().setFitScale(true, true /*allowZoomIn*/);
+ }
+ }
+
+ reloadPalette();
+
+ getCanvasControl().getPreviewManager().configurationChanged(flags);
+
+ return true;
+ }
+
+ @Override
+ public void setActivity(@NonNull String activity) {
+ ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject());
+ String pkg = manifest.getPackage();
+ if (activity.startsWith(pkg) && activity.length() > pkg.length()
+ && activity.charAt(pkg.length()) == '.') {
+ activity = activity.substring(pkg.length());
+ }
+ CommonXmlEditor editor = getEditorDelegate().getEditor();
+ Element element = editor.getUiRootNode().getXmlDocument().getDocumentElement();
+ AdtUtils.setToolsAttribute(editor,
+ element, "Choose Activity", ATTR_CONTEXT,
+ activity, false /*reveal*/, false /*append*/);
+ }
+
+ /**
+ * Returns a {@link ProjectResources} for the framework resources based on the current
+ * configuration selection.
+ * @return the framework resources or null if not found.
+ */
+ @Override
+ @Nullable
+ public ResourceRepository getFrameworkResources() {
+ return getFrameworkResources(getRenderingTarget());
+ }
+
+ /**
+ * Returns a {@link ProjectResources} for the framework resources of a given
+ * target.
+ * @param target the target for which to return the framework resources.
+ * @return the framework resources or null if not found.
+ */
+ @Override
+ @Nullable
+ public ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target) {
+ if (target != null) {
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+
+ if (data != null) {
+ return data.getFrameworkResources();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public ProjectResources getProjectResources() {
+ if (mEditedFile != null) {
+ ResourceManager manager = ResourceManager.getInstance();
+ return manager.getProjectResources(mEditedFile.getProject());
+ }
+
+ return null;
+ }
+
+
+ @Override
+ @NonNull
+ public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() {
+ if (mConfiguredFrameworkRes == null && mConfigChooser != null) {
+ ResourceRepository frameworkRes = getFrameworkResources();
+
+ if (frameworkRes == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
+ } else {
+ // get the framework resource values based on the current config
+ mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
+ mConfigChooser.getConfiguration().getFullConfig());
+ }
+ }
+
+ return mConfiguredFrameworkRes;
+ }
+
+ @Override
+ @NonNull
+ public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() {
+ if (mConfiguredProjectRes == null && mConfigChooser != null) {
+ ProjectResources project = getProjectResources();
+
+ // get the project resource values based on the current config
+ mConfiguredProjectRes = project.getConfiguredResources(
+ mConfigChooser.getConfiguration().getFullConfig());
+ }
+
+ return mConfiguredProjectRes;
+ }
+
+ @Override
+ public void createConfigFile() {
+ LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigChooser.getShell(),
+ mEditedFile.getName(), mConfigChooser.getConfiguration().getFullConfig());
+ if (dialog.open() != Window.OK) {
+ return;
+ }
+
+ FolderConfiguration config = new FolderConfiguration();
+ dialog.getConfiguration(config);
+
+ // Creates a new layout file from the specified {@link FolderConfiguration}.
+ CreateNewConfigJob job = new CreateNewConfigJob(this, mEditedFile, config);
+ job.schedule();
+ }
+
+ /**
+ * Returns the resource name of the file that is including this current layout, if any
+ * (may be null)
+ *
+ * @return the resource name of an including layout, or null
+ */
+ @Override
+ public Reference getIncludedWithin() {
+ return mIncludedWithin;
+ }
+
+ @Override
+ @Nullable
+ public LayoutCanvas getCanvas() {
+ return getCanvasControl();
+ }
+
+ /**
+ * Listens to target changed in the current project, to trigger a new layout rendering.
+ */
+ private class TargetListener implements ITargetChangeListener {
+
+ @Override
+ public void onProjectTargetChange(IProject changedProject) {
+ if (changedProject != null && changedProject.equals(getProject())) {
+ updateEditor();
+ }
+ }
+
+ @Override
+ public void onTargetLoaded(IAndroidTarget loadedTarget) {
+ IAndroidTarget target = getRenderingTarget();
+ if (target != null && target.equals(loadedTarget)) {
+ updateEditor();
+ }
+ }
+
+ @Override
+ public void onSdkLoaded() {
+ // get the current rendering target to unload it
+ IAndroidTarget oldTarget = getRenderingTarget();
+ preRenderingTargetChangeCleanUp(oldTarget);
+
+ computeSdkVersion();
+
+ // get the project target
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+ if (target != null) {
+ mConfigChooser.onSdkLoaded(target);
+ changed(CFG_FOLDER | CFG_TARGET);
+ }
+ }
+ }
+
+ private void updateEditor() {
+ mEditorDelegate.getEditor().commitPages(false /* onSave */);
+
+ // because the target changed we must reset the configured resources.
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+ mResourceResolver = null;
+
+ // make sure we remove the custom view loader, since its parent class loader is the
+ // bridge class loader.
+ mProjectCallback = null;
+
+ // recreate the ui root node always, this will also call onTargetChange
+ // on the config composite
+ mEditorDelegate.delegateInitUiRootNode(true /*force*/);
+ }
+
+ private IProject getProject() {
+ return getEditorDelegate().getEditor().getProject();
+ }
+ }
+
+ /** Refresh the configured project resources associated with this editor */
+ public void refreshProjectResources() {
+ mConfiguredProjectRes = null;
+ mResourceResolver = null;
+ }
+
+ /**
+ * Returns the currently edited file
+ *
+ * @return the currently edited file, or null
+ */
+ public IFile getEditedFile() {
+ return mEditedFile;
+ }
+
+ /**
+ * Returns the project for the currently edited file, or null
+ *
+ * @return the project containing the edited file, or null
+ */
+ public IProject getProject() {
+ if (mEditedFile != null) {
+ return mEditedFile.getProject();
+ } else {
+ return null;
+ }
+ }
+
+ // ----------------
+
+ /**
+ * Save operation in the Graphical Editor Part.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ * <p/>
+ * This must NOT call the parent editor part. At the contrary, the parent editor
+ * part will call this *after* having done the actual save operation.
+ * <p/>
+ * The only action this editor must do is mark the undo command stack as
+ * being no longer dirty.
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ // TODO implement a command stack
+// getCommandStack().markSaveLocation();
+// firePropertyChange(PROP_DIRTY);
+ }
+
+ /**
+ * Save operation in the Graphical Editor Part.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public void doSaveAs() {
+ // pass
+ }
+
+ /**
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public boolean isDirty() {
+ return false;
+ }
+
+ /**
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ @Override
+ public void setFocus() {
+ // TODO Auto-generated method stub
+
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the activated page.
+ */
+ public void activated() {
+ if (!mActive) {
+ mActive = true;
+
+ syncDockingState();
+ mActionBar.updateErrorIndicator();
+
+ boolean changed = mConfigChooser.syncRenderState();
+ if (changed) {
+ // Will also force recomputeLayout()
+ return;
+ }
+
+ if (mNeedsRecompute) {
+ recomputeLayout();
+ }
+
+ mCanvasViewer.getCanvas().syncPreviewMode();
+ }
+ }
+
+ /**
+ * The global docking state version. This number is incremented each time
+ * the user customizes the window layout in any layout.
+ */
+ private static int sDockingStateVersion;
+
+ /**
+ * The window docking state version that this window is currently showing;
+ * when a different window is reconfigured, the global version number is
+ * incremented, and when this window is shown, and the current version is
+ * less than the global version, the window layout will be synced.
+ */
+ private int mDockingStateVersion;
+
+ /**
+ * Syncs the window docking state.
+ * <p>
+ * The layout editor lets you change the docking state -- e.g. you can minimize the
+ * palette, and drag the structure view to the bottom, and so on. When you restart
+ * the IDE, the window comes back up with your customized state.
+ * <p>
+ * <b>However</b>, when you have multiple editor files open, if you minimize the palette
+ * in one editor and then switch to another, the other editor will have the old window
+ * state. That's because each editor has its own set of windows.
+ * <p>
+ * This method fixes this. Whenever a window is shown, this method is called, and the
+ * docking state is synced such that the editor will match the current persistent docking
+ * state.
+ */
+ private void syncDockingState() {
+ if (mDockingStateVersion == sDockingStateVersion) {
+ // No changes to apply
+ return;
+ }
+ mDockingStateVersion = sDockingStateVersion;
+
+ IPreferenceStore preferenceStore = AdtPlugin.getDefault().getPreferenceStore();
+ PluginFlyoutPreferences preferences;
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_PALETTE);
+ mPaletteComposite.apply(preferences);
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_STRUCTURE);
+ mStructureFlyout.apply(preferences);
+ mPaletteComposite.layout();
+ mStructureFlyout.layout();
+ mPaletteComposite.redraw(); // the structure view is nested within the palette
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the deactivated page
+ */
+ public void deactivated() {
+ mActive = false;
+
+ LayoutCanvas canvas = getCanvasControl();
+ if (canvas != null) {
+ canvas.deactivated();
+ }
+ }
+
+ /**
+ * Opens and initialize the editor with a new file.
+ * @param file the file being edited.
+ */
+ public void openFile(IFile file) {
+ mEditedFile = file;
+ mConfigChooser.setFile(mEditedFile);
+
+ if (mReloadListener == null) {
+ mReloadListener = new ReloadListener();
+ LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener);
+ }
+
+ if (mRulesEngine == null) {
+ mRulesEngine = new RulesEngine(this, mEditedFile.getProject());
+ if (mCanvasViewer != null) {
+ mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine);
+ }
+ }
+
+ // Pick up hand-off data: somebody requesting this file to be opened may have
+ // requested that it should be opened as included within another file
+ if (mEditedFile != null) {
+ try {
+ mIncludedWithin = (Reference) mEditedFile.getSessionProperty(NAME_INCLUDE);
+ if (mIncludedWithin != null) {
+ // Only use once
+ mEditedFile.setSessionProperty(NAME_INCLUDE, null);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't access session property %1$s", NAME_INCLUDE);
+ }
+ }
+
+ computeSdkVersion();
+ }
+
+ /**
+ * Resets the editor with a replacement file.
+ * @param file the replacement file.
+ */
+ public void replaceFile(IFile file) {
+ mEditedFile = file;
+ mConfigChooser.replaceFile(mEditedFile);
+ computeSdkVersion();
+ }
+
+ /**
+ * Resets the editor with a replacement file coming from a config change in the config
+ * selector.
+ * @param file the replacement file.
+ */
+ public void changeFileOnNewConfig(IFile file) {
+ mEditedFile = file;
+ mConfigChooser.changeFileOnNewConfig(mEditedFile);
+ }
+
+ /**
+ * Responds to a target change for the project of the edited file
+ */
+ public void onTargetChange() {
+ AndroidTargetData targetData = mConfigChooser.onXmlModelLoaded();
+ updateCapabilities(targetData);
+
+ changed(CFG_FOLDER | CFG_TARGET);
+ }
+
+ /** Updates the capabilities for the given target data (which may be null) */
+ private void updateCapabilities(AndroidTargetData targetData) {
+ if (targetData != null) {
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ if (mIncludedWithin != null && !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) {
+ showIn(null);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link CommonXmlDelegate} for this editor
+ *
+ * @return the {@link CommonXmlDelegate} for this editor
+ */
+ @NonNull
+ public LayoutEditorDelegate getEditorDelegate() {
+ return mEditorDelegate;
+ }
+
+ /**
+ * Returns the {@link RulesEngine} associated with this editor
+ *
+ * @return the {@link RulesEngine} associated with this editor, never null
+ */
+ public RulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+
+ /**
+ * Return the {@link LayoutCanvas} associated with this editor
+ *
+ * @return the associated {@link LayoutCanvas}
+ */
+ public LayoutCanvas getCanvasControl() {
+ if (mCanvasViewer != null) {
+ return mCanvasViewer.getCanvas();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link UiDocumentNode} for the XML model edited by this editor
+ *
+ * @return the associated model
+ */
+ public UiDocumentNode getModel() {
+ return mEditorDelegate.getUiRootNode();
+ }
+
+ /**
+ * Callback for XML model changed. Only update/recompute the layout if the editor is visible
+ */
+ public void onXmlModelChanged() {
+ // To optimize the rendering when the user is editing in the XML pane, we don't
+ // refresh the editor if it's not the active part.
+ //
+ // This behavior is acceptable when the editor is the single "full screen" part
+ // (as in this case active means visible.)
+ // Unfortunately this breaks in 2 cases:
+ // - when performing a drag'n'drop from one editor to another, the target is not
+ // properly refreshed before it becomes active.
+ // - when duplicating the editor window and placing both editors side by side (xml in one
+ // and canvas in the other one), the canvas may not be refreshed when the XML is edited.
+ //
+ // TODO find a way to really query whether the pane is visible, not just active.
+
+ if (mEditorDelegate.isGraphicalEditorActive()) {
+ recomputeLayout();
+ } else {
+ // Remember we want to recompute as soon as the editor becomes active.
+ mNeedsRecompute = true;
+ }
+ }
+
+ /**
+ * Recomputes the layout
+ */
+ public void recomputeLayout() {
+ try {
+ if (!ensureFileValid()) {
+ return;
+ }
+
+ UiDocumentNode model = getModel();
+ LayoutCanvas canvas = mCanvasViewer.getCanvas();
+ if (!ensureModelValid(model)) {
+ // Although we display an error, we still treat an empty document as a
+ // successful layout result so that we can drop new elements in it.
+ //
+ // For that purpose, create a special LayoutScene that has no image,
+ // no root view yet indicates success and then update the canvas with it.
+
+ canvas.setSession(
+ new StaticRenderSession(
+ Result.Status.SUCCESS.createResult(),
+ null /*rootViewInfo*/, null /*image*/),
+ null /*explodeNodes*/, true /* layoutlib5 */);
+ return;
+ }
+
+ LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/);
+
+ if (layoutLib != null) {
+ // if drawing in real size, (re)set the scaling factor.
+ if (mActionBar.isZoomingRealSize()) {
+ mActionBar.computeAndSetRealScale(false /* redraw */);
+ }
+
+ IProject project = mEditedFile.getProject();
+ renderWithBridge(project, model, layoutLib);
+
+ canvas.getPreviewManager().renderPreviews();
+ }
+ } finally {
+ // no matter the result, we are done doing the recompute based on the latest
+ // resource/code change.
+ mNeedsRecompute = false;
+ }
+ }
+
+ /**
+ * Reloads the palette
+ */
+ public void reloadPalette() {
+ if (mPalette != null) {
+ IAndroidTarget renderingTarget = getRenderingTarget();
+ if (renderingTarget != null) {
+ mPalette.reloadPalette(renderingTarget);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link LayoutLibrary} associated with this editor, if it has
+ * been initialized already. May return null if it has not been initialized (or has
+ * not finished initializing).
+ *
+ * @return The {@link LayoutLibrary}, or null
+ */
+ public LayoutLibrary getLayoutLibrary() {
+ return getReadyLayoutLib(false /*displayError*/);
+ }
+
+ /**
+ * Returns the scale to multiply pixels in the layout coordinate space with to obtain
+ * the corresponding dip (device independent pixel)
+ *
+ * @return the scale to multiple layout coordinates with to obtain the dip position
+ */
+ public float getDipScale() {
+ float dpi = mConfigChooser.getConfiguration().getDensity().getDpiValue();
+ return Density.DEFAULT_DENSITY / dpi;
+ }
+
+ // --- private methods ---
+
+ /**
+ * Ensure that the file associated with this editor is valid (exists and is
+ * synchronized). Any reasons why it is not are displayed in the editor's error area.
+ *
+ * @return True if the editor is valid, false otherwise.
+ */
+ private boolean ensureFileValid() {
+ // check that the resource exists. If the file is opened but the project is closed
+ // or deleted for some reason (changed from outside of eclipse), then this will
+ // return false;
+ if (mEditedFile.exists() == false) {
+ displayError("Resource '%1$s' does not exist.",
+ mEditedFile.getFullPath().toString());
+ return false;
+ }
+
+ if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
+ String message = String.format("%1$s is out of sync. Please refresh.",
+ mEditedFile.getName());
+
+ displayError(message);
+
+ // also print it in the error console.
+ IProject iProject = mEditedFile.getProject();
+ AdtPlugin.printErrorToConsole(iProject.getName(), message);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a {@link LayoutLibrary} that is ready for rendering, or null if the bridge
+ * is not available or not ready yet (due to SDK loading still being in progress etc).
+ * If enabled, any reasons preventing the bridge from being returned are displayed to the
+ * editor's error area.
+ *
+ * @param displayError whether to display the loading error or not.
+ *
+ * @return LayoutBridge the layout bridge for rendering this editor's scene
+ */
+ LayoutLibrary getReadyLayoutLib(boolean displayError) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = getRenderingTarget();
+
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ LayoutLibrary layoutLib = data.getLayoutLibrary();
+
+ if (layoutLib.getStatus() == LoadStatus.LOADED) {
+ return layoutLib;
+ } else if (displayError) { // getBridge() == null
+ // SDK is loaded but not the layout library!
+
+ // check whether the bridge managed to load, or not
+ if (layoutLib.getStatus() == LoadStatus.LOADING) {
+ displayError("Eclipse is loading framework information and the layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ } else {
+ String message = layoutLib.getLoadMessage();
+ displayError("Eclipse failed to load the framework information and the layout library!" +
+ message != null ? "\n" + message : "");
+ }
+ }
+ } else { // data == null
+ // It can happen that the workspace refreshes while the SDK is loading its
+ // data, which could trigger a redraw of the opened layout if some resources
+ // changed while Eclipse is closed.
+ // In this case data could be null, but this is not an error.
+ // We can just silently return, as all the opened editors are automatically
+ // refreshed once the SDK finishes loading.
+ LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);
+
+ // display error is asked.
+ if (displayError) {
+ String targetName = target.getName();
+ switch (targetLoadStatus) {
+ case LOADING:
+ String s;
+ if (currentSdk.getTarget(getProject()) == target) {
+ s = String.format(
+ "The project target (%1$s) is still loading.",
+ targetName);
+ } else {
+ s = String.format(
+ "The rendering target (%1$s) is still loading.",
+ targetName);
+ }
+ s += "\nThe layout will refresh automatically once the process is finished.";
+ displayError(s);
+
+ break;
+ case FAILED: // known failure
+ case LOADED: // success but data isn't loaded?!?!
+ displayError("The project target (%s) was not properly loaded.",
+ targetName);
+ break;
+ }
+ }
+ }
+
+ } else if (displayError) { // target == null
+ displayError("The project target is not set. Right click project, choose Properties | Android.");
+ }
+ } else if (displayError) { // currentSdk == null
+ displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link IAndroidTarget} used for the rendering.
+ * <p/>
+ * This first looks for the rendering target setup in the config UI, and if nothing has
+ * been setup yet, returns the target of the project.
+ *
+ * @return an IAndroidTarget object or null if no target is setup and the project has no
+ * target set.
+ *
+ */
+ public IAndroidTarget getRenderingTarget() {
+ // if the SDK is null no targets are loaded.
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk == null) {
+ return null;
+ }
+
+ // attempt to get a target from the configuration selector.
+ IAndroidTarget renderingTarget = mConfigChooser.getConfiguration().getTarget();
+ if (renderingTarget != null) {
+ return renderingTarget;
+ }
+
+ // fall back to the project target
+ if (mEditedFile != null) {
+ return currentSdk.getTarget(mEditedFile.getProject());
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the current rendering target supports the given capability
+ *
+ * @param capability the capability to be looked up
+ * @return true if the current rendering target supports the given capability
+ */
+ public boolean renderingSupports(Capability capability) {
+ IAndroidTarget target = getRenderingTarget();
+ if (target != null) {
+ AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ return layoutLib.supports(capability);
+ }
+
+ return false;
+ }
+
+ private boolean ensureModelValid(UiDocumentNode model) {
+ // check there is actually a model (maybe the file is empty).
+ if (model.getUiChildren().size() == 0) {
+ if (mEditorDelegate.getEditor().isCreatingPages()) {
+ displayError("Loading editor");
+ return false;
+ }
+ displayError(
+ "No XML content. Please add a root view or layout to your document.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a {@link RenderService} associated with this editor
+ * @return the render service
+ */
+ @NonNull
+ public RenderService createRenderService() {
+ return RenderService.create(this, mCredential);
+ }
+
+ /**
+ * Creates a {@link RenderLogger} associated with this editor
+ * @param name the name of the logger
+ * @return the new logger
+ */
+ @NonNull
+ public RenderLogger createRenderLogger(String name) {
+ return new RenderLogger(name, mCredential);
+ }
+
+ /**
+ * Creates a {@link RenderService} associated with this editor
+ *
+ * @param configuration the configuration to use (and fallback to editor for the rest)
+ * @param resolver a resource resolver to use to look up resources
+ * @return the render service
+ */
+ @NonNull
+ public RenderService createRenderService(Configuration configuration,
+ ResourceResolver resolver) {
+ return RenderService.create(this, configuration, resolver, mCredential);
+ }
+
+ private void renderWithBridge(IProject iProject, UiDocumentNode model,
+ LayoutLibrary layoutLib) {
+ LayoutCanvas canvas = getCanvasControl();
+ Set<UiElementNode> explodeNodes = canvas.getNodesToExplode();
+ RenderLogger logger = createRenderLogger(mEditedFile.getName());
+ RenderingMode renderingMode = RenderingMode.NORMAL;
+ // FIXME set the rendering mode using ViewRule or something.
+ List<UiElementNode> children = model.getUiChildren();
+ if (children.size() > 0 &&
+ children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) {
+ renderingMode = RenderingMode.V_SCROLL;
+ }
+
+ RenderSession session = RenderService.create(this, mCredential)
+ .setModel(model)
+ .setLog(logger)
+ .setRenderingMode(renderingMode)
+ .setIncludedWithin(mIncludedWithin)
+ .setNodesToExpand(explodeNodes)
+ .createRenderSession();
+
+ boolean layoutlib5 = layoutLib.supports(Capability.EMBEDDED_LAYOUT);
+ canvas.setSession(session, explodeNodes, layoutlib5);
+
+ // update the UiElementNode with the layout info.
+ if (session != null && session.getResult().isSuccess() == false) {
+ // An error was generated. Print it (and any other accumulated warnings)
+ String errorMessage = session.getResult().getErrorMessage();
+ Throwable exception = session.getResult().getException();
+ if (exception != null && errorMessage == null) {
+ errorMessage = exception.toString();
+ }
+ if (exception != null || (errorMessage != null && errorMessage.length() > 0)) {
+ logger.error(null, errorMessage, exception, null /*data*/);
+ } else if (!logger.hasProblems()) {
+ logger.error(null, "Unexpected error in rendering, no details given",
+ null /*data*/);
+ }
+ // These errors will be included in the log warnings which are
+ // displayed regardless of render success status below
+ }
+
+ // We might have detected some missing classes and swapped them by a mock view,
+ // or run into fidelity warnings or missing resources, so emit all these
+ // warnings
+ Set<String> missingClasses = mProjectCallback.getMissingClasses();
+ Set<String> brokenClasses = mProjectCallback.getUninstantiatableClasses();
+ if (logger.hasProblems()) {
+ displayLoggerProblems(iProject, logger);
+ displayFailingClasses(missingClasses, brokenClasses, true);
+ displayUserStackTrace(logger, true);
+ } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) {
+ displayFailingClasses(missingClasses, brokenClasses, false);
+ displayUserStackTrace(logger, true);
+ } else if (session != null) {
+ // Nope, no missing or broken classes. Clear success, congrats!
+ hideError();
+
+ // First time this layout is opened, run lint on the file (after a delay)
+ if (!mRenderedOnce) {
+ mRenderedOnce = true;
+ Job job = new Job("Run Lint") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ getEditorDelegate().delegateRunLint();
+ return Status.OK_STATUS;
+ }
+
+ };
+ job.setSystem(true);
+ job.schedule(3000); // 3 seconds
+ }
+
+ mConfigChooser.ensureInitialized();
+ }
+
+ model.refreshUi();
+ }
+
+ /**
+ * Returns the {@link ResourceResolver} for this editor
+ *
+ * @return the resolver used to resolve resources for the current configuration of
+ * this editor, or null
+ */
+ public ResourceResolver getResourceResolver() {
+ if (mResourceResolver == null) {
+ String theme = mConfigChooser.getThemeName();
+ if (theme == null) {
+ displayError("Missing theme.");
+ return null;
+ }
+ boolean isProjectTheme = mConfigChooser.getConfiguration().isProjectTheme();
+
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
+ getConfiguredProjectResources();
+
+ // Get the framework resources
+ Map<ResourceType, Map<String, ResourceValue>> frameworkResources =
+ getConfiguredFrameworkResources();
+
+ if (configuredProjectRes == null) {
+ displayError("Missing project resources for current configuration.");
+ return null;
+ }
+
+ if (frameworkResources == null) {
+ displayError("Missing framework resources.");
+ return null;
+ }
+
+ mResourceResolver = ResourceResolver.create(
+ configuredProjectRes, frameworkResources,
+ theme, isProjectTheme);
+ }
+
+ return mResourceResolver;
+ }
+
+ /** Returns a project callback, and optionally resets it */
+ ProjectCallback getProjectCallback(boolean reset, LayoutLibrary layoutLibrary) {
+ // Lazily create the project callback the first time we need it
+ if (mProjectCallback == null) {
+ ResourceManager resManager = ResourceManager.getInstance();
+ IProject project = getProject();
+ ProjectResources projectRes = resManager.getProjectResources(project);
+ mProjectCallback = new ProjectCallback(layoutLibrary, projectRes, project,
+ mCredential, this);
+ } else if (reset) {
+ // Also clears the set of missing/broken classes prior to rendering
+ mProjectCallback.getMissingClasses().clear();
+ mProjectCallback.getUninstantiatableClasses().clear();
+ }
+
+ return mProjectCallback;
+ }
+
+ /**
+ * Returns the resource name of this layout, NOT including the @layout/ prefix
+ *
+ * @return the resource name of this layout, NOT including the @layout/ prefix
+ */
+ public String getLayoutResourceName() {
+ return ResourceHelper.getLayoutName(mEditedFile);
+ }
+
+ /**
+ * Cleans up when the rendering target is about to change
+ * @param oldTarget the old rendering target.
+ */
+ private void preRenderingTargetChangeCleanUp(IAndroidTarget oldTarget) {
+ // first clear the caches related to this file in the old target
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ AndroidTargetData data = currentSdk.getTargetData(oldTarget);
+ if (data != null) {
+ LayoutLibrary layoutLib = data.getLayoutLibrary();
+
+ // layoutLib can never be null.
+ layoutLib.clearCaches(mEditedFile.getProject());
+ }
+ }
+
+ // Also remove the ProjectCallback as it caches custom views which must be reloaded
+ // with the classloader of the new LayoutLib. We also have to clear it out
+ // because it stores a reference to the layout library which could have changed.
+ mProjectCallback = null;
+
+ // FIXME: get rid of the current LayoutScene if any.
+ }
+
+ private class ReloadListener implements ILayoutReloadListener {
+ /**
+ * Called when the file changes triggered a redraw of the layout
+ */
+ @Override
+ public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) {
+ if (mConfigChooser.isDisposed()) {
+ return;
+ }
+ Display display = mConfigChooser.getDisplay();
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ reloadLayoutSwt(flags, libraryChanged);
+ }
+ });
+ }
+
+ /** Reload layout. <b>Must be called on the SWT thread</b> */
+ private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) {
+ if (mConfigChooser.isDisposed()) {
+ return;
+ }
+ assert mConfigChooser.getDisplay().getThread() == Thread.currentThread();
+
+ boolean recompute = false;
+ // we only care about the r class of the main project.
+ if (flags.rClass && libraryChanged == false) {
+ recompute = true;
+ if (mEditedFile != null) {
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources projectRes = manager.getProjectResources(
+ mEditedFile.getProject());
+
+ if (projectRes != null) {
+ projectRes.resetDynamicIds();
+ }
+ }
+ }
+
+ if (flags.localeList) {
+ // the locale list *potentially* changed so we update the locale in the
+ // config composite.
+ // However there's no recompute, as it could not be needed
+ // (for instance a new layout)
+ // If a resource that's not a layout changed this will trigger a recompute anyway.
+ mConfigChooser.updateLocales();
+ }
+
+ // if a resources was modified.
+ if (flags.resources) {
+ recompute = true;
+
+ // TODO: differentiate between single and multi resource file changed, and whether
+ // the resource change affects the cache.
+
+ // force a reparse in case a value XML file changed.
+ mConfiguredProjectRes = null;
+ mResourceResolver = null;
+
+ // clear the cache in the bridge in case a bitmap/9-patch changed.
+ LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/);
+ if (layoutLib != null) {
+ layoutLib.clearCaches(mEditedFile.getProject());
+ }
+ }
+
+ if (flags.code) {
+ // only recompute if the custom view loader was used to load some code.
+ if (mProjectCallback != null && mProjectCallback.isUsed()) {
+ mProjectCallback = null;
+ recompute = true;
+ }
+ }
+
+ if (flags.manifest) {
+ recompute |= computeSdkVersion();
+ }
+
+ if (recompute) {
+ if (mEditorDelegate.isGraphicalEditorActive()) {
+ recomputeLayout();
+ } else {
+ mNeedsRecompute = true;
+ }
+ }
+ }
+ }
+
+ // ---- Error handling ----
+
+ /**
+ * Switches the sash to display the error label.
+ *
+ * @param errorFormat The new error to display if not null.
+ * @param parameters String.format parameters for the error format.
+ */
+ private void displayError(String errorFormat, Object...parameters) {
+ if (errorFormat != null) {
+ mErrorLabel.setText(String.format(errorFormat, parameters));
+ } else {
+ mErrorLabel.setText("");
+ }
+ mSashError.setMaximizedControl(null);
+ }
+
+ /** Displays the canvas and hides the error label. */
+ private void hideError() {
+ mErrorLabel.setText("");
+ mSashError.setMaximizedControl(mCanvasViewer.getControl());
+ }
+
+ /** Display the problem list encountered during a render */
+ private void displayUserStackTrace(RenderLogger logger, boolean append) {
+ List<Throwable> throwables = logger.getFirstTrace();
+ if (throwables == null || throwables.isEmpty()) {
+ return;
+ }
+
+ Throwable throwable = throwables.get(0);
+
+ if (throwable instanceof RenderSecurityException) {
+ addActionLink(mErrorLabel, ActionLinkStyleRange.LINK_DISABLE_SANDBOX,
+ "\nTurn off custom view rendering sandbox\n");
+
+ StringBuilder builder = new StringBuilder(200);
+ String lastFailedPath = RenderSecurityManager.getLastFailedPath();
+ if (lastFailedPath != null) {
+ builder.append("Diagnostic info for ADT bug report:\n");
+ builder.append("Failed path: ").append(lastFailedPath).append('\n');
+ String tempDir = System.getProperty("java.io.tmpdir");
+ builder.append("Normal temp dir: ").append(tempDir).append('\n');
+ File normalized = new File(tempDir);
+ builder.append("Normalized temp dir: ").append(normalized.getPath()).append('\n');
+ try {
+ builder.append("Canonical temp dir: ").append(normalized.getCanonicalPath())
+ .append('\n');
+ } catch (IOException e) {
+ // ignore
+ }
+ builder.append("os.name: ").append(System.getProperty("os.name")).append('\n');
+ builder.append("os.version: ").append(System.getProperty("os.version"));
+ builder.append('\n');
+ builder.append("java.runtime.version: ");
+ builder.append(System.getProperty("java.runtime.version"));
+ }
+ if (throwable.getMessage().equals("Unable to create temporary file")) {
+ String javaVersion = System.getProperty("java.version");
+ if (javaVersion.startsWith("1.7.0_")) {
+ int version = Integer
+ .parseInt(javaVersion.substring(javaVersion.indexOf('_') + 1));
+ if (version > 0 && version < 45) {
+ builder.append('\n');
+ builder.append("Tip: This may be caused by using an older version " +
+ "of JDK 1.7.0; try using at least 1.7.0_45 (you are using " +
+ javaVersion + ")");
+ }
+ }
+ }
+ if (builder.length() > 0) {
+ addText(mErrorLabel, builder.toString());
+ }
+ }
+
+ StackTraceElement[] frames = throwable.getStackTrace();
+ int end = -1;
+ boolean haveInterestingFrame = false;
+ for (int i = 0; i < frames.length; i++) {
+ StackTraceElement frame = frames[i];
+ if (isInterestingFrame(frame)) {
+ haveInterestingFrame = true;
+ }
+ String className = frame.getClassName();
+ if (className.equals(
+ "com.android.layoutlib.bridge.impl.RenderSessionImpl")) { //$NON-NLS-1$
+ end = i;
+ break;
+ }
+ }
+
+ if (end == -1 || !haveInterestingFrame) {
+ // Not a recognized stack trace range: just skip it
+ return;
+ }
+
+ if (!append) {
+ mErrorLabel.setText("\n"); //$NON-NLS-1$
+ } else {
+ addText(mErrorLabel, "\n\n"); //$NON-NLS-1$
+ }
+
+ addText(mErrorLabel, throwable.toString() + '\n');
+ for (int i = 0; i < end; i++) {
+ StackTraceElement frame = frames[i];
+ String className = frame.getClassName();
+ String methodName = frame.getMethodName();
+ addText(mErrorLabel, " at " + className + '.' + methodName + '(');
+ String fileName = frame.getFileName();
+ if (fileName != null && !fileName.isEmpty()) {
+ int lineNumber = frame.getLineNumber();
+ String location = fileName + ':' + lineNumber;
+ if (isInterestingFrame(frame)) {
+ addActionLink(mErrorLabel, ActionLinkStyleRange.LINK_OPEN_LINE,
+ location, className, methodName, fileName, lineNumber);
+ } else {
+ addText(mErrorLabel, location);
+ }
+ addText(mErrorLabel, ")\n"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ private static boolean isInterestingFrame(StackTraceElement frame) {
+ String className = frame.getClassName();
+ return !(className.startsWith("android.") //$NON-NLS-1$
+ || className.startsWith("com.android.") //$NON-NLS-1$
+ || className.startsWith("java.") //$NON-NLS-1$
+ || className.startsWith("javax.") //$NON-NLS-1$
+ || className.startsWith("sun.")); //$NON-NLS-1$
+ }
+
+ /**
+ * Switches the sash to display the error label to show a list of
+ * missing classes and give options to create them.
+ */
+ private void displayFailingClasses(Set<String> missingClasses, Set<String> brokenClasses,
+ boolean append) {
+ if (missingClasses.size() == 0 && brokenClasses.size() == 0) {
+ return;
+ }
+
+ if (!append) {
+ mErrorLabel.setText(""); //$NON-NLS-1$
+ } else {
+ addText(mErrorLabel, "\n"); //$NON-NLS-1$
+ }
+
+ if (missingClasses.size() > 0) {
+ addText(mErrorLabel, "The following classes could not be found:\n");
+ for (String clazz : missingClasses) {
+ addText(mErrorLabel, "- ");
+ addText(mErrorLabel, clazz);
+ addText(mErrorLabel, " (");
+
+ IProject project = getProject();
+ Collection<String> customViews = getCustomViewClassNames(project);
+ addTypoSuggestions(clazz, customViews, false);
+ addTypoSuggestions(clazz, customViews, true);
+ addTypoSuggestions(clazz, getAndroidViewClassNames(project), false);
+
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_FIX_BUILD_PATH, "Fix Build Path", clazz);
+ addText(mErrorLabel, ", ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_EDIT_XML, "Edit XML", clazz);
+ if (clazz.indexOf('.') != -1) {
+ // Add "Create Class" link, but only for custom views
+ addText(mErrorLabel, ", ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_CREATE_CLASS, "Create Class", clazz);
+ }
+ addText(mErrorLabel, ")\n");
+ }
+ }
+ if (brokenClasses.size() > 0) {
+ addText(mErrorLabel, "The following classes could not be instantiated:\n");
+
+ // Do we have a custom class (not an Android or add-ons class)
+ boolean haveCustomClass = false;
+
+ for (String clazz : brokenClasses) {
+ addText(mErrorLabel, "- ");
+ addText(mErrorLabel, clazz);
+ addText(mErrorLabel, " (");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_OPEN_CLASS, "Open Class", clazz);
+ addText(mErrorLabel, ", ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_SHOW_LOG, "Show Error Log", clazz);
+ addText(mErrorLabel, ")\n");
+
+ if (!(clazz.startsWith("android.") || //$NON-NLS-1$
+ clazz.startsWith("com.google."))) { //$NON-NLS-1$
+ haveCustomClass = true;
+ }
+ }
+
+ addText(mErrorLabel, "See the Error Log (Window > Show View) for more details.\n");
+
+ if (haveCustomClass) {
+ addBoldText(mErrorLabel, "Tip: Use View.isInEditMode() in your custom views "
+ + "to skip code when shown in Eclipse");
+ }
+ }
+
+ mSashError.setMaximizedControl(null);
+ }
+
+ private void addTypoSuggestions(String actual, Collection<String> views,
+ boolean compareWithPackage) {
+ if (views.size() == 0) {
+ return;
+ }
+
+ // Look for typos and try to match with custom views and android views
+ String actualBase = actual.substring(actual.lastIndexOf('.') + 1);
+ int maxDistance = actualBase.length() >= 4 ? 2 : 1;
+
+ if (views.size() > 0) {
+ for (String suggested : views) {
+ String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1);
+
+ String matchWith = compareWithPackage ? suggested : suggestedBase;
+ if (Math.abs(actualBase.length() - matchWith.length()) > maxDistance) {
+ // The string lengths differ more than the allowed edit distance;
+ // no point in even attempting to compute the edit distance (requires
+ // O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
+ continue;
+ }
+ if (LintUtils.editDistance(actualBase, matchWith) <= maxDistance) {
+ // Suggest this class as a typo for the given class
+ String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1)
+ ? suggested : suggestedBase;
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_CHANGE_CLASS_TO,
+ String.format("Change to %1$s",
+ // Only show full package name if class name
+ // is the same
+ labelClass),
+ actual,
+ viewNeedsPackage(suggested) ? suggested : suggestedBase);
+ addText(mErrorLabel, ", ");
+ }
+ }
+ }
+ }
+
+ private static Collection<String> getCustomViewClassNames(IProject project) {
+ CustomViewFinder finder = CustomViewFinder.get(project);
+ Collection<String> views = finder.getAllViews();
+ if (views == null) {
+ finder.refresh();
+ views = finder.getAllViews();
+ }
+
+ return views;
+ }
+
+ private static Collection<String> getAndroidViewClassNames(IProject project) {
+ Sdk currentSdk = Sdk.getCurrent();
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData targetData = currentSdk.getTargetData(target);
+ if (targetData != null) {
+ LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors();
+ return layoutDescriptors.getAllViewClassNames();
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Add a normal line of text to the styled text widget. */
+ private void addText(StyledText styledText, String...string) {
+ for (String s : string) {
+ styledText.append(s);
+ }
+ }
+
+ /** Display the problem list encountered during a render */
+ private void displayLoggerProblems(IProject project, RenderLogger logger) {
+ if (logger.hasProblems()) {
+ mErrorLabel.setText("");
+ // A common source of problems is attempting to open a layout when there are
+ // compilation errors. In this case, may not have run (or may not be up to date)
+ // so resources cannot be looked up etc. Explain this situation to the user.
+
+ boolean hasAaptErrors = false;
+ boolean hasJavaErrors = false;
+ try {
+ IMarker[] markers;
+ markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
+ if (markers.length > 0) {
+ for (IMarker marker : markers) {
+ String markerType = marker.getType();
+ if (markerType.equals(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER)) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (severity == IMarker.SEVERITY_ERROR) {
+ hasJavaErrors = true;
+ }
+ } else if (markerType.equals(AdtConstants.MARKER_AAPT_COMPILE)) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (severity == IMarker.SEVERITY_ERROR) {
+ hasAaptErrors = true;
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ if (logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
+ addBoldText(mErrorLabel,
+ "Missing styles. Is the correct theme chosen for this layout?\n");
+ addText(mErrorLabel,
+ "Use the Theme combo box above the layout to choose a different layout, " +
+ "or fix the theme style references.\n\n");
+ }
+
+ List<Throwable> trace = logger.getFirstTrace();
+ if (trace != null
+ && trace.toString().contains(
+ "java.lang.IndexOutOfBoundsException: Index: 2, Size: 2") //$NON-NLS-1$
+ && mConfigChooser.getConfiguration().getDensity() == Density.TV) {
+ addBoldText(mErrorLabel,
+ "It looks like you are using a render target where the layout library " +
+ "does not support the tvdpi density.\n\n");
+ addText(mErrorLabel, "Please try either updating to " +
+ "the latest available version (using the SDK manager), or if no updated " +
+ "version is available for this specific version of Android, try using " +
+ "a more recent render target version.\n\n");
+
+ }
+
+ if (hasAaptErrors && logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_PREFIX)) {
+ // Text will automatically be wrapped by the error widget so no reason
+ // to insert linebreaks in this error message:
+ String message =
+ "NOTE: This project contains resource errors, so aapt did not succeed, "
+ + "which can cause rendering failures. "
+ + "Fix resource problems first.\n\n";
+ addBoldText(mErrorLabel, message);
+ } else if (hasJavaErrors && mProjectCallback != null && mProjectCallback.isUsed()) {
+ // Text will automatically be wrapped by the error widget so no reason
+ // to insert linebreaks in this error message:
+ String message =
+ "NOTE: This project contains Java compilation errors, "
+ + "which can cause rendering failures for custom views. "
+ + "Fix compilation problems first.\n\n";
+ addBoldText(mErrorLabel, message);
+ }
+
+ if (logger.seenTag(RenderLogger.TAG_MISSING_DIMENSION)) {
+ List<UiElementNode> elements = UiDocumentNode.getAllElements(getModel());
+ for (UiElementNode element : elements) {
+ String width = element.getAttributeValue(ATTR_LAYOUT_WIDTH);
+ if (width == null || width.length() == 0) {
+ addSetAttributeLink(element, ATTR_LAYOUT_WIDTH);
+ }
+
+ String height = element.getAttributeValue(ATTR_LAYOUT_HEIGHT);
+ if (height == null || height.length() == 0) {
+ addSetAttributeLink(element, ATTR_LAYOUT_HEIGHT);
+ }
+ }
+ }
+
+ String problems = logger.getProblems(false /*includeFidelityWarnings*/);
+ addText(mErrorLabel, problems);
+
+ List<String> fidelityWarnings = logger.getFidelityWarnings();
+ if (fidelityWarnings != null && fidelityWarnings.size() > 0) {
+ addText(mErrorLabel,
+ "The graphics preview in the layout editor may not be accurate:\n");
+ for (String warning : fidelityWarnings) {
+ addText(mErrorLabel, warning + ' ');
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.IGNORE_FIDELITY_WARNING,
+ "(Ignore for this session)\n", warning);
+ }
+ }
+
+ mSashError.setMaximizedControl(null);
+ } else {
+ mSashError.setMaximizedControl(mCanvasViewer.getControl());
+ }
+ }
+
+ /** Appends an action link to set the given attribute on the given value */
+ private void addSetAttributeLink(UiElementNode element, String attribute) {
+ if (element.getXmlNode().getNodeName().equals(GRID_LAYOUT)) {
+ // GridLayout does not require a layout_width or layout_height to be defined
+ return;
+ }
+
+ String fill = VALUE_FILL_PARENT;
+ // See whether we should offer match_parent instead of fill_parent
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(getProject());
+ if (target.getVersion().getApiLevel() >= 8) {
+ fill = VALUE_MATCH_PARENT;
+ }
+ }
+
+ String id = element.getAttributeValue(ATTR_ID);
+ if (id == null || id.length() == 0) {
+ id = '<' + element.getXmlNode().getNodeName() + '>';
+ } else {
+ id = BaseLayoutRule.stripIdPrefix(id);
+ }
+
+ addText(mErrorLabel, String.format("\"%1$s\" does not set the required %2$s attribute:\n",
+ id, attribute));
+ addText(mErrorLabel, " (1) ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.SET_ATTRIBUTE,
+ String.format("Set to \"%1$s\"", VALUE_WRAP_CONTENT),
+ element, attribute, VALUE_WRAP_CONTENT);
+ addText(mErrorLabel, "\n (2) ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.SET_ATTRIBUTE,
+ String.format("Set to \"%1$s\"\n", fill),
+ element, attribute, fill);
+ }
+
+ /** Appends the given text as a bold string in the given text widget */
+ private void addBoldText(StyledText styledText, String text) {
+ String s = styledText.getText();
+ int start = (s == null ? 0 : s.length());
+
+ styledText.append(text);
+ StyleRange sr = new StyleRange();
+ sr.start = start;
+ sr.length = text.length();
+ sr.fontStyle = SWT.BOLD;
+ styledText.setStyleRange(sr);
+ }
+
+ /**
+ * Add a URL-looking link to the styled text widget.
+ * <p/>
+ * A mouse-click listener is setup and it interprets the link based on the
+ * action, corresponding to the value fields in {@link ActionLinkStyleRange}.
+ */
+ private void addActionLink(StyledText styledText, int action, String label,
+ Object... data) {
+ String s = styledText.getText();
+ int start = (s == null ? 0 : s.length());
+ styledText.append(label);
+
+ StyleRange sr = new ActionLinkStyleRange(action, data);
+ sr.start = start;
+ sr.length = label.length();
+ sr.fontStyle = SWT.NORMAL;
+ sr.underlineStyle = SWT.UNDERLINE_LINK;
+ sr.underline = true;
+ styledText.setStyleRange(sr);
+ }
+
+ /**
+ * Looks up the resource file corresponding to the given type
+ *
+ * @param type The type of resource to look up, such as {@link ResourceType#LAYOUT}
+ * @param name The name of the resource (not including ".xml")
+ * @param isFrameworkResource if true, the resource is a framework resource, otherwise
+ * it's a project resource
+ * @return the resource file defining the named resource, or null if not found
+ */
+ public IPath findResourceFile(ResourceType type, String name, boolean isFrameworkResource) {
+ // FIXME: This code does not handle theme value resolution.
+ // There is code to handle this, but it's in layoutlib; we should
+ // expose that and use it here.
+
+ Map<ResourceType, Map<String, ResourceValue>> map;
+ map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes;
+ if (map == null) {
+ // Not yet configured
+ return null;
+ }
+
+ Map<String, ResourceValue> layoutMap = map.get(type);
+ if (layoutMap != null) {
+ ResourceValue value = layoutMap.get(name);
+ if (value != null) {
+ String valueStr = value.getValue();
+ if (valueStr.startsWith("?")) { //$NON-NLS-1$
+ // FIXME: It's a reference. We should resolve this properly.
+ return null;
+ }
+ return new Path(valueStr);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Looks up the path to the file corresponding to the given attribute value, such as
+ * @layout/foo, which will return the foo.xml file in res/layout/. (The general format
+ * of the resource url is {@literal @[<package_name>:]<resource_type>/<resource_name>}.
+ *
+ * @param url the attribute url
+ * @return the path to the file defining this attribute, or null if not found
+ */
+ public IPath findResourceFile(String url) {
+ if (!url.startsWith("@")) { //$NON-NLS-1$
+ return null;
+ }
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd == -1) {
+ return null;
+ }
+ int nameBegin = typeEnd + 1;
+ int typeBegin = 1;
+ int colon = url.lastIndexOf(':', typeEnd);
+ boolean isFrameworkResource = false;
+ if (colon != -1) {
+ // The URL contains a package name.
+ // While the url format technically allows other package names,
+ // the platform apparently only supports @android for now (or if it does,
+ // there are no usages in the current code base so this is not common).
+ String packageName = url.substring(typeBegin, colon);
+ if (ANDROID_PKG.equals(packageName)) {
+ isFrameworkResource = true;
+ }
+
+ typeBegin = colon + 1;
+ }
+
+ String typeName = url.substring(typeBegin, typeEnd);
+ ResourceType type = ResourceType.getEnum(typeName);
+ if (type == null) {
+ return null;
+ }
+
+ String name = url.substring(nameBegin);
+ return findResourceFile(type, name, isFrameworkResource);
+ }
+
+ /**
+ * Resolve the given @string reference into a literal String using the current project
+ * configuration
+ *
+ * @param text the text resource reference to resolve
+ * @return the resolved string, or null
+ */
+ public String findString(String text) {
+ if (text.startsWith(STRING_PREFIX)) {
+ return findString(text.substring(STRING_PREFIX.length()), false);
+ } else if (text.startsWith(ANDROID_STRING_PREFIX)) {
+ return findString(text.substring(ANDROID_STRING_PREFIX.length()), true);
+ } else {
+ return text;
+ }
+ }
+
+ private String findString(String name, boolean isFrameworkResource) {
+ Map<ResourceType, Map<String, ResourceValue>> map;
+ map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes;
+ if (map == null) {
+ // Not yet configured
+ return null;
+ }
+
+ Map<String, ResourceValue> layoutMap = map.get(ResourceType.STRING);
+ if (layoutMap != null) {
+ ResourceValue value = layoutMap.get(name);
+ if (value != null) {
+ // FIXME: This code does not handle theme value resolution.
+ // There is code to handle this, but it's in layoutlib; we should
+ // expose that and use it here.
+ return value.getValue();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This StyleRange represents a clickable link in the render output, where various
+ * actions can be taken such as creating a class, opening the project chooser to
+ * adjust the build path, etc.
+ */
+ private class ActionLinkStyleRange extends StyleRange {
+ /** Create a view class */
+ private static final int LINK_CREATE_CLASS = 1;
+ /** Edit the build path for the current project */
+ private static final int LINK_FIX_BUILD_PATH = 2;
+ /** Show the XML tab */
+ private static final int LINK_EDIT_XML = 3;
+ /** Open the given class */
+ private static final int LINK_OPEN_CLASS = 4;
+ /** Show the error log */
+ private static final int LINK_SHOW_LOG = 5;
+ /** Change the class reference to the given fully qualified name */
+ private static final int LINK_CHANGE_CLASS_TO = 6;
+ /** Ignore the given fidelity warning */
+ private static final int IGNORE_FIDELITY_WARNING = 7;
+ /** Set an attribute on the given XML element to a given value */
+ private static final int SET_ATTRIBUTE = 8;
+ /** Open the given file and line number */
+ private static final int LINK_OPEN_LINE = 9;
+ /** Disable sandbox */
+ private static final int LINK_DISABLE_SANDBOX = 10;
+
+ /** Client data: the contents depend on the specific action */
+ private final Object[] mData;
+ /** The action to be taken when the link is clicked */
+ private final int mAction;
+
+ private ActionLinkStyleRange(int action, Object... data) {
+ super();
+ mAction = action;
+ mData = data;
+ }
+
+ /** Performs the click action */
+ public void onClick() {
+ switch (mAction) {
+ case LINK_CREATE_CLASS:
+ createNewClass((String) mData[0]);
+ break;
+ case LINK_EDIT_XML:
+ mEditorDelegate.getEditor().setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
+ break;
+ case LINK_FIX_BUILD_PATH:
+ @SuppressWarnings("restriction")
+ String id = BuildPathsPropertyPage.PROP_ID;
+ PreferencesUtil.createPropertyDialogOn(
+ AdtPlugin.getShell(),
+ getProject(), id, null, null).open();
+ break;
+ case LINK_OPEN_CLASS:
+ AdtPlugin.openJavaClass(getProject(), (String) mData[0]);
+ break;
+ case LINK_OPEN_LINE:
+ boolean success = AdtPlugin.openStackTraceLine(
+ (String) mData[0], // class
+ (String) mData[1], // method
+ (String) mData[2], // file
+ (Integer) mData[3]); // line
+ if (!success) {
+ MessageDialog.openError(mErrorLabel.getShell(), "Not Found",
+ String.format("Could not find %1$s.%2$s", mData[0], mData[1]));
+ }
+ break;
+ case LINK_SHOW_LOG:
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow();
+ try {
+ IWorkbenchPage page = workbenchWindow.getActivePage();
+ page.showView("org.eclipse.pde.runtime.LogView"); //$NON-NLS-1$
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ break;
+ case LINK_CHANGE_CLASS_TO:
+ // Change class reference of mData[0] to mData[1]
+ // TODO: run under undo lock
+ MultiTextEdit edits = new MultiTextEdit();
+ ISourceViewer textViewer =
+ mEditorDelegate.getEditor().getStructuredSourceViewer();
+ IDocument document = textViewer.getDocument();
+ String xml = document.get();
+ int index = 0;
+ // Replace <old with <new and </old with </new
+ String prefix = "<"; //$NON-NLS-1$
+ String find = prefix + mData[0];
+ String replaceWith = prefix + mData[1];
+ while (true) {
+ index = xml.indexOf(find, index);
+ if (index == -1) {
+ break;
+ }
+ edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
+ index += find.length();
+ }
+ index = 0;
+ prefix = "</"; //$NON-NLS-1$
+ find = prefix + mData[0];
+ replaceWith = prefix + mData[1];
+ while (true) {
+ index = xml.indexOf(find, index);
+ if (index == -1) {
+ break;
+ }
+ edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
+ index += find.length();
+ }
+ // Handle <view class="old">
+ index = 0;
+ prefix = "\""; //$NON-NLS-1$
+ String suffix = "\""; //$NON-NLS-1$
+ find = prefix + mData[0] + suffix;
+ replaceWith = prefix + mData[1] + suffix;
+ while (true) {
+ index = xml.indexOf(find, index);
+ if (index == -1) {
+ break;
+ }
+ edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
+ index += find.length();
+ }
+ try {
+ edits.apply(document);
+ } catch (MalformedTreeException e) {
+ AdtPlugin.log(e, null);
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ break;
+ case IGNORE_FIDELITY_WARNING:
+ RenderLogger.ignoreFidelityWarning((String) mData[0]);
+ recomputeLayout();
+ break;
+ case SET_ATTRIBUTE: {
+ final UiElementNode element = (UiElementNode) mData[0];
+ final String attribute = (String) mData[1];
+ final String value = (String) mData[2];
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(
+ String.format("Set \"%1$s\" to \"%2$s\"", attribute, value),
+ new Runnable() {
+ @Override
+ public void run() {
+ element.setAttributeValue(attribute, ANDROID_URI, value, true);
+ element.commitDirtyAttributesToXml();
+ }
+ });
+ break;
+ }
+ case LINK_DISABLE_SANDBOX: {
+ RenderSecurityManager.sEnabled = false;
+ recomputeLayout();
+
+ MessageDialog.openInformation(AdtPlugin.getShell(),
+ "Disabled Rendering Sandbox",
+ "The custom view rendering sandbox was disabled for this session.\n\n" +
+ "You can turn it off permanently by adding\n" +
+ "-D" + ENABLED_PROPERTY + "=" + VALUE_FALSE + "\n" +
+ "as a new line in eclipse.ini.");
+
+ break;
+ }
+ default:
+ assert false : mAction;
+ break;
+ }
+ }
+
+ @Override
+ public boolean similarTo(StyleRange style) {
+ // Prevent adjacent link ranges from getting merged
+ return false;
+ }
+ }
+
+ /**
+ * Returns the error label for the graphical editor (which may not be visible
+ * or showing errors)
+ *
+ * @return the error label, never null
+ */
+ StyledText getErrorLabel() {
+ return mErrorLabel;
+ }
+
+ /**
+ * Monitor clicks on the error label.
+ * If the click happens on a style range created by
+ * {@link GraphicalEditorPart#addClassLink(StyledText, String)}, we assume it's about
+ * a missing class and we then proceed to display the standard Eclipse class creator wizard.
+ */
+ private class ErrorLabelListener extends MouseAdapter {
+
+ @Override
+ public void mouseUp(MouseEvent event) {
+ super.mouseUp(event);
+
+ if (event.widget != mErrorLabel) {
+ return;
+ }
+
+ int offset = mErrorLabel.getCaretOffset();
+
+ StyleRange r = null;
+ StyleRange[] ranges = mErrorLabel.getStyleRanges();
+ if (ranges != null && ranges.length > 0) {
+ for (StyleRange sr : ranges) {
+ if (sr.start <= offset && sr.start + sr.length > offset) {
+ r = sr;
+ break;
+ }
+ }
+ }
+
+ if (r instanceof ActionLinkStyleRange) {
+ ActionLinkStyleRange range = (ActionLinkStyleRange) r;
+ range.onClick();
+ }
+
+ LayoutCanvas canvas = getCanvasControl();
+ canvas.updateMenuActionState();
+ }
+ }
+
+ private void createNewClass(String fqcn) {
+
+ int pos = fqcn.lastIndexOf('.');
+ String packageName = pos < 0 ? "" : fqcn.substring(0, pos); //$NON-NLS-1$
+ String className = pos <= 0 || pos >= fqcn.length() ? "" : fqcn.substring(pos + 1); //$NON-NLS-1$
+
+ // create the wizard page for the class creation, and configure it
+ NewClassWizardPage page = new NewClassWizardPage();
+
+ // set the parent class
+ page.setSuperClass(SdkConstants.CLASS_VIEW, true /* canBeModified */);
+
+ // get the source folders as java elements.
+ IPackageFragmentRoot[] roots = getPackageFragmentRoots(
+ mEditorDelegate.getEditor().getProject(),
+ false /*includeContainers*/, true /*skipGenFolder*/);
+
+ IPackageFragmentRoot currentRoot = null;
+ IPackageFragment currentFragment = null;
+ int packageMatchCount = -1;
+
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ // we have a perfect match! we use it.
+ currentRoot = root;
+ currentFragment = fragment;
+ packageMatchCount = -1;
+ break;
+ } else {
+ // we don't have a match. we look for the fragment with the best match
+ // (ie the closest parent package we can find)
+ try {
+ IJavaElement[] children;
+ children = root.getChildren();
+ for (IJavaElement child : children) {
+ if (child instanceof IPackageFragment) {
+ fragment = (IPackageFragment)child;
+ if (packageName.startsWith(fragment.getElementName())) {
+ // its a match. get the number of segments
+ String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
+ if (segments.length > packageMatchCount) {
+ packageMatchCount = segments.length;
+ currentFragment = fragment;
+ currentRoot = root;
+ }
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ // Couldn't get the children: we just ignore this package root.
+ }
+ }
+ }
+
+ ArrayList<IPackageFragment> createdFragments = null;
+
+ if (currentRoot != null) {
+ // if we have a perfect match, we set it and we're done.
+ if (packageMatchCount == -1) {
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(currentFragment, true /* canBeModified */);
+ } else {
+ // we have a partial match.
+ // create the package. We have to start with the first segment so that we
+ // know what to delete in case of a cancel.
+ try {
+ createdFragments = new ArrayList<IPackageFragment>();
+
+ int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
+ int count = 0;
+ int index = -1;
+ // skip the matching packages
+ while (count < packageMatchCount) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ }
+
+ // create the rest of the segments, except for the last one as indexOf will
+ // return -1;
+ while (count < totalCount - 1) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName.substring(0, index),
+ true /* force*/, new NullProgressMonitor()));
+ }
+
+ // create the last package
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName, true /* force*/, new NullProgressMonitor()));
+
+ // set the root and fragment in the Wizard page
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
+ true /* canBeModified */);
+ } catch (JavaModelException e) {
+ // If we can't create the packages, there's a problem.
+ // We revert to the default package
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ page.setPackageFragmentRoot(root, true /* canBeModified*/);
+ page.setPackageFragment(fragment, true /* canBeModified */);
+ break;
+ }
+ }
+ }
+ }
+ } else if (roots.length > 0) {
+ // if we haven't found a valid fragment, we set the root to the first source folder.
+ page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
+ }
+
+ // if we have a starting class name we use it
+ if (className != null) {
+ page.setTypeName(className, true /* canBeModified*/);
+ }
+
+ // create the action that will open it the wizard.
+ OpenNewClassWizardAction action = new OpenNewClassWizardAction();
+ action.setConfiguredWizardPage(page);
+ action.run();
+ IJavaElement element = action.getCreatedElement();
+
+ if (element == null) {
+ // lets delete the packages we created just for this.
+ // we need to start with the leaf and go up
+ if (createdFragments != null) {
+ try {
+ for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
+ createdFragments.get(i).delete(true /* force*/,
+ new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source
+ * folders of the specified project.
+ *
+ * @param project the project
+ * @param includeContainers True to include containers
+ * @param skipGenFolder True to skip the "gen" folder
+ * @return an array of IPackageFragmentRoot.
+ */
+ private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
+ boolean includeContainers, boolean skipGenFolder) {
+ ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
+ try {
+ IJavaProject javaProject = JavaCore.create(project);
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+ for (int i = 0; i < roots.length; i++) {
+ if (skipGenFolder) {
+ IResource resource = roots[i].getResource();
+ if (resource != null && resource.getName().equals(FD_GEN_SOURCES)) {
+ continue;
+ }
+ }
+ IClasspathEntry entry = roots[i].getRawClasspathEntry();
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
+ (includeContainers &&
+ entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
+ result.add(roots[i]);
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return result.toArray(new IPackageFragmentRoot[result.size()]);
+ }
+
+ /**
+ * Reopens this file as included within the given file (this assumes that the given
+ * file has an include tag referencing this view, and the set of views that have this
+ * property can be found using the {@link IncludeFinder}.
+ *
+ * @param includeWithin reference to a file to include as a surrounding context,
+ * or null to show the file standalone
+ */
+ public void showIn(Reference includeWithin) {
+ mIncludedWithin = includeWithin;
+
+ if (includeWithin != null) {
+ IFile file = includeWithin.getFile();
+
+ // Update configuration
+ if (file != null) {
+ mConfigChooser.resetConfigFor(file);
+ }
+ }
+ recomputeLayout();
+ }
+
+ /**
+ * Return all resource names of a given type, either in the project or in the
+ * framework.
+ *
+ * @param framework if true, return all the framework resource names, otherwise return
+ * all the project resource names
+ * @param type the type of resource to look up
+ * @return a collection of resource names, never null but possibly empty
+ */
+ public Collection<String> getResourceNames(boolean framework, ResourceType type) {
+ Map<ResourceType, Map<String, ResourceValue>> map =
+ framework ? mConfiguredFrameworkRes : mConfiguredProjectRes;
+ Map<String, ResourceValue> animations = map.get(type);
+ if (animations != null) {
+ return animations.keySet();
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Return this editor's current configuration
+ *
+ * @return the current configuration
+ */
+ public FolderConfiguration getConfiguration() {
+ return mConfigChooser.getConfiguration().getFullConfig();
+ }
+
+ /**
+ * Figures out the project's minSdkVersion and targetSdkVersion and return whether the values
+ * have changed.
+ */
+ private boolean computeSdkVersion() {
+ int oldMinSdkVersion = mMinSdkVersion;
+ int oldTargetSdkVersion = mTargetSdkVersion;
+
+ Pair<Integer, Integer> v = ManifestInfo.computeSdkVersions(mEditedFile.getProject());
+ mMinSdkVersion = v.getFirst();
+ mTargetSdkVersion = v.getSecond();
+
+ return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion;
+ }
+
+ /**
+ * Returns the associated configuration chooser
+ *
+ * @return the configuration chooser
+ */
+ @NonNull
+ public ConfigurationChooser getConfigurationChooser() {
+ return mConfigChooser;
+ }
+
+ /**
+ * Returns the associated layout actions bar
+ *
+ * @return the layout actions bar
+ */
+ @NonNull
+ public LayoutActionBar getLayoutActionBar() {
+ return mActionBar;
+ }
+
+ /**
+ * Returns the target SDK version
+ *
+ * @return the target SDK version
+ */
+ public int getTargetSdkVersion() {
+ return mTargetSdkVersion;
+ }
+
+ /**
+ * Returns the minimum SDK version
+ *
+ * @return the minimum SDK version
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /** If the flyout hover is showing, dismiss it */
+ public void dismissHoverPalette() {
+ mPaletteComposite.dismissHover();
+ }
+
+ // ---- Implements IFlyoutListener ----
+
+ @Override
+ public void stateChanged(int oldState, int newState) {
+ // Auto zoom the surface if you open or close flyout windows such as the palette
+ // or the property/outline views
+ if (newState == STATE_OPEN || newState == STATE_COLLAPSED && oldState == STATE_OPEN) {
+ getCanvasControl().setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/);
+ }
+
+ sDockingStateVersion++;
+ mDockingStateVersion = sDockingStateVersion;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java
new file mode 100644
index 000000000..2e7c559db
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtDrawingStyle.HOVER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtDrawingStyle.HOVER_SELECTION;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.List;
+
+/**
+ * The {@link HoverOverlay} paints an optional hover on top of the layout,
+ * highlighting the currently hovered view.
+ */
+public class HoverOverlay extends Overlay {
+ private final LayoutCanvas mCanvas;
+
+ /** Hover border color. Must be disposed, it's NOT a system color. */
+ private Color mHoverStrokeColor;
+
+ /** Hover fill color. Must be disposed, it's NOT a system color. */
+ private Color mHoverFillColor;
+
+ /** Hover border select color. Must be disposed, it's NOT a system color. */
+ private Color mHoverSelectStrokeColor;
+
+ /** Hover fill select color. Must be disposed, it's NOT a system color. */
+ private Color mHoverSelectFillColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Current mouse hover border rectangle. Null when there's no mouse hover.
+ * The rectangle coordinates do not take account of the translation, which
+ * must be applied to the rectangle when drawing.
+ */
+ private Rectangle mHoverRect;
+
+ /**
+ * Constructs a new {@link HoverOverlay} linked to the given view hierarchy.
+ *
+ * @param canvas the associated canvas
+ * @param hScale The {@link CanvasTransform} to use to transfer horizontal layout
+ * coordinates to screen coordinates.
+ * @param vScale The {@link CanvasTransform} to use to transfer vertical layout
+ * coordinates to screen coordinates.
+ */
+ public HoverOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) {
+ mCanvas = canvas;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ if (SwtDrawingStyle.HOVER.getStrokeColor() != null) {
+ mHoverStrokeColor = new Color(device, SwtDrawingStyle.HOVER.getStrokeColor());
+ }
+ if (SwtDrawingStyle.HOVER.getFillColor() != null) {
+ mHoverFillColor = new Color(device, SwtDrawingStyle.HOVER.getFillColor());
+ }
+
+ if (SwtDrawingStyle.HOVER_SELECTION.getStrokeColor() != null) {
+ mHoverSelectStrokeColor = new Color(device,
+ SwtDrawingStyle.HOVER_SELECTION.getStrokeColor());
+ }
+ if (SwtDrawingStyle.HOVER_SELECTION.getFillColor() != null) {
+ mHoverSelectFillColor = new Color(device,
+ SwtDrawingStyle.HOVER_SELECTION.getFillColor());
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (mHoverStrokeColor != null) {
+ mHoverStrokeColor.dispose();
+ mHoverStrokeColor = null;
+ }
+
+ if (mHoverFillColor != null) {
+ mHoverFillColor.dispose();
+ mHoverFillColor = null;
+ }
+
+ if (mHoverSelectStrokeColor != null) {
+ mHoverSelectStrokeColor.dispose();
+ mHoverSelectStrokeColor = null;
+ }
+
+ if (mHoverSelectFillColor != null) {
+ mHoverSelectFillColor.dispose();
+ mHoverSelectFillColor = null;
+ }
+ }
+
+ /**
+ * Sets the hover rectangle. The coordinates of the rectangle are in layout
+ * coordinates. The recipient is will own this rectangle.
+ * <p/>
+ * TODO: Consider switching input arguments to two {@link LayoutPoint}s so
+ * we don't have ambiguity about the coordinate system of these input
+ * parameters.
+ * <p/>
+ *
+ * @param x The top left x coordinate, in layout coordinates, of the hover.
+ * @param y The top left y coordinate, in layout coordinates, of the hover.
+ * @param w The width of the hover (in layout coordinates).
+ * @param h The height of the hover (in layout coordinates).
+ */
+ public void setHover(int x, int y, int w, int h) {
+ mHoverRect = new Rectangle(x, y, w, h);
+ }
+
+ /**
+ * Removes the hover for the next paint.
+ */
+ public void clearHover() {
+ mHoverRect = null;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mHoverRect != null) {
+ // Translate the hover rectangle (in canvas coordinates) to control
+ // coordinates
+ int x = mHScale.translate(mHoverRect.x);
+ int y = mVScale.translate(mHoverRect.y);
+ int w = mHScale.scale(mHoverRect.width);
+ int h = mVScale.scale(mHoverRect.height);
+
+
+ boolean hoverIsSelected = false;
+ List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections();
+ for (SelectionItem item : selections) {
+ if (mHoverRect.equals(item.getViewInfo().getSelectionRect())) {
+ hoverIsSelected = true;
+ break;
+ }
+ }
+
+ Color stroke = hoverIsSelected ? mHoverSelectStrokeColor : mHoverStrokeColor;
+ Color fill = hoverIsSelected ? mHoverSelectFillColor : mHoverFillColor;
+
+ if (stroke != null) {
+ int oldAlpha = gc.getAlpha();
+ gc.setForeground(stroke);
+ gc.setLineStyle(hoverIsSelected ?
+ HOVER_SELECTION.getLineStyle() : HOVER.getLineStyle());
+ gc.setAlpha(hoverIsSelected ?
+ HOVER_SELECTION.getStrokeAlpha() : HOVER.getStrokeAlpha());
+ gc.drawRectangle(x, y, w, h);
+ gc.setAlpha(oldAlpha);
+ }
+
+ if (fill != null) {
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(hoverIsSelected ?
+ HOVER_SELECTION.getFillAlpha() : HOVER.getFillAlpha());
+ gc.setBackground(fill);
+ gc.fillRectangle(x, y, w, h);
+ gc.setAlpha(oldAlpha);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java
new file mode 100644
index 000000000..4447eebd2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * An ImageControl which simply renders an image, with optional margins and tooltips. This
+ * is useful since a {@link CLabel}, even without text, will hide the image when there is
+ * not enough room to fully fit it.
+ * <p>
+ * The image is always rendered left and top aligned.
+ */
+public class ImageControl extends Canvas implements MouseTrackListener {
+ private Image mImage;
+ private int mLeftMargin;
+ private int mTopMargin;
+ private int mRightMargin;
+ private int mBottomMargin;
+ private boolean mDisposeImage = true;
+ private boolean mMouseIn;
+ private Color mHoverColor;
+ private float mScale = 1.0f;
+
+ /**
+ * Creates an ImageControl rendering the given image, which will be disposed when this
+ * control is disposed (unless the {@link #setDisposeImage} method is called to turn
+ * off auto dispose).
+ *
+ * @param parent the parent to add the image control to
+ * @param style the SWT style to use
+ * @param image the image to be rendered, which must not be null and should be unique
+ * for this image control since it will be disposed by this control when
+ * the control is disposed (unless the {@link #setDisposeImage} method is
+ * called to turn off auto dispose)
+ */
+ public ImageControl(@NonNull Composite parent, int style, @Nullable Image image) {
+ super(parent, style | SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED);
+ mImage = image;
+
+ addPaintListener(new PaintListener() {
+ @Override
+ public void paintControl(PaintEvent event) {
+ onPaint(event);
+ }
+ });
+ }
+
+ @Nullable
+ public Image getImage() {
+ return mImage;
+ }
+
+ public void setImage(@Nullable Image image) {
+ if (mDisposeImage && mImage != null) {
+ mImage.dispose();
+ }
+ mImage = image;
+ redraw();
+ }
+
+ public void fitToWidth(int width) {
+ if (mImage == null) {
+ return;
+ }
+ Rectangle imageRect = mImage.getBounds();
+ int imageWidth = imageRect.width;
+ if (imageWidth <= width) {
+ mScale = 1.0f;
+ return;
+ }
+
+ mScale = width / (float) imageWidth;
+ redraw();
+ }
+
+ public void setScale(float scale) {
+ mScale = scale;
+ }
+
+ public float getScale() {
+ return mScale;
+ }
+
+ public void setHoverColor(@Nullable Color hoverColor) {
+ if (mHoverColor != null) {
+ removeMouseTrackListener(this);
+ }
+ mHoverColor = hoverColor;
+ if (hoverColor != null) {
+ addMouseTrackListener(this);
+ }
+ }
+
+ @Nullable
+ public Color getHoverColor() {
+ return mHoverColor;
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mDisposeImage && mImage != null && !mImage.isDisposed()) {
+ mImage.dispose();
+ }
+ mImage = null;
+ }
+
+ public void setDisposeImage(boolean disposeImage) {
+ mDisposeImage = disposeImage;
+ }
+
+ public boolean getDisposeImage() {
+ return mDisposeImage;
+ }
+
+ @Override
+ public Point computeSize(int wHint, int hHint, boolean changed) {
+ checkWidget();
+ Point e = new Point(0, 0);
+ if (mImage != null) {
+ Rectangle r = mImage.getBounds();
+ if (mScale != 1.0f) {
+ e.x += mScale * r.width;
+ e.y += mScale * r.height;
+ } else {
+ e.x += r.width;
+ e.y += r.height;
+ }
+ }
+ if (wHint == SWT.DEFAULT) {
+ e.x += mLeftMargin + mRightMargin;
+ } else {
+ e.x = wHint;
+ }
+ if (hHint == SWT.DEFAULT) {
+ e.y += mTopMargin + mBottomMargin;
+ } else {
+ e.y = hHint;
+ }
+
+ return e;
+ }
+
+ private void onPaint(PaintEvent event) {
+ Rectangle rect = getClientArea();
+ if (mImage == null || rect.width == 0 || rect.height == 0) {
+ return;
+ }
+
+ GC gc = event.gc;
+ Rectangle imageRect = mImage.getBounds();
+ int imageHeight = imageRect.height;
+ int imageWidth = imageRect.width;
+ int destWidth = imageWidth;
+ int destHeight = imageHeight;
+
+ int oldGcAlias = gc.getAntialias();
+ int oldGcInterpolation = gc.getInterpolation();
+ if (mScale != 1.0f) {
+ destWidth = (int) (mScale * destWidth);
+ destHeight = (int) (mScale * destHeight);
+ gc.setAntialias(SWT.ON);
+ gc.setInterpolation(SWT.HIGH);
+ }
+
+ gc.drawImage(mImage, 0, 0, imageWidth, imageHeight, rect.x + mLeftMargin, rect.y
+ + mTopMargin, destWidth, destHeight);
+
+ gc.setAntialias(oldGcAlias);
+ gc.setInterpolation(oldGcInterpolation);
+
+ if (mHoverColor != null && mMouseIn) {
+ gc.setAlpha(60);
+ gc.setBackground(mHoverColor);
+ gc.setLineWidth(1);
+ gc.fillRectangle(0, 0, destWidth, destHeight);
+ }
+ }
+
+ public void setMargins(int leftMargin, int topMargin, int rightMargin, int bottomMargin) {
+ checkWidget();
+ mLeftMargin = Math.max(0, leftMargin);
+ mTopMargin = Math.max(0, topMargin);
+ mRightMargin = Math.max(0, rightMargin);
+ mBottomMargin = Math.max(0, bottomMargin);
+ redraw();
+ }
+
+ // ---- Implements MouseTrackListener ----
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ mMouseIn = true;
+ if (mHoverColor != null) {
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ mMouseIn = false;
+ if (mHoverColor != null) {
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java
new file mode 100644
index 000000000..a1363ecb1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+
+import com.android.SdkConstants;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.api.IImageFactory;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.lang.ref.SoftReference;
+
+/**
+ * The {@link ImageOverlay} class renders an image as an overlay.
+ */
+public class ImageOverlay extends Overlay implements IImageFactory {
+ /**
+ * Whether the image should be pre-scaled (scaled to the zoom level) once
+ * instead of dynamically during each paint; this is necessary on some
+ * platforms (see issue #19447)
+ */
+ private static final boolean PRESCALE =
+ // Currently this is necessary on Linux because the "Cairo" library
+ // seems to be a bottleneck
+ SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX
+ && !(Boolean.getBoolean("adt.noprescale")); //$NON-NLS-1$
+
+ /** Current background image. Null when there's no image. */
+ private Image mImage;
+
+ /** A pre-scaled version of the image */
+ private Image mPreScaledImage;
+
+ /** Whether the rendered image should have a drop shadow */
+ private boolean mShowDropShadow;
+
+ /** Current background AWT image. This is created by {@link #getImage()}, which is called
+ * by the LayoutLib. */
+ private SoftReference<BufferedImage> mAwtImage = new SoftReference<BufferedImage>(null);
+
+ /**
+ * Strong reference to the image in the above soft reference, to prevent
+ * garbage collection when {@link PRESCALE} is set, until the scaled image
+ * is created (lazily as part of the next paint call, where this strong
+ * reference is nulled out and the above soft reference becomes eligible to
+ * be reclaimed when memory is low.)
+ */
+ @SuppressWarnings("unused") // Used by the garbage collector to keep mAwtImage non-soft
+ private BufferedImage mAwtImageStrongRef;
+
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Constructs an {@link ImageOverlay} tied to the given canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+ * @param hScale The horizontal scale information.
+ * @param vScale The vertical scale information.
+ */
+ public ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) {
+ mCanvas = canvas;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ super.create(device);
+ }
+
+ @Override
+ public void dispose() {
+ if (mImage != null) {
+ mImage.dispose();
+ mImage = null;
+ }
+ if (mPreScaledImage != null) {
+ mPreScaledImage.dispose();
+ mPreScaledImage = null;
+ }
+ }
+
+ /**
+ * Sets the image to be drawn as an overlay from the passed in AWT
+ * {@link BufferedImage} (which will be converted to an SWT image).
+ * <p/>
+ * The image <b>can</b> be null, which is the case when we are dealing with
+ * an empty document.
+ *
+ * @param awtImage The AWT image to be rendered as an SWT image.
+ * @param isAlphaChannelImage whether the alpha channel of the image is relevant
+ * @return The corresponding SWT image, or null.
+ */
+ public synchronized Image setImage(BufferedImage awtImage, boolean isAlphaChannelImage) {
+ mShowDropShadow = !isAlphaChannelImage;
+
+ BufferedImage oldAwtImage = mAwtImage.get();
+ if (awtImage != oldAwtImage || awtImage == null) {
+ mAwtImage.clear();
+ mAwtImageStrongRef = null;
+
+ if (mImage != null) {
+ mImage.dispose();
+ }
+
+ if (awtImage == null) {
+ mImage = null;
+ } else {
+ mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage,
+ isAlphaChannelImage, -1);
+ }
+ } else {
+ assert awtImage instanceof SwtReadyBufferedImage;
+
+ if (isAlphaChannelImage) {
+ if (mImage != null) {
+ mImage.dispose();
+ }
+
+ mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, true, -1);
+ } else {
+ Image prev = mImage;
+ mImage = ((SwtReadyBufferedImage)awtImage).getSwtImage();
+ if (prev != mImage && prev != null) {
+ prev.dispose();
+ }
+ }
+ }
+
+ if (mPreScaledImage != null) {
+ // Force refresh on next paint
+ mPreScaledImage.dispose();
+ mPreScaledImage = null;
+ }
+
+ return mImage;
+ }
+
+ /**
+ * Returns the currently painted image, or null if none has been set
+ *
+ * @return the currently painted image or null
+ */
+ public Image getImage() {
+ return mImage;
+ }
+
+ /**
+ * Returns the currently rendered image, or null if none has been set
+ *
+ * @return the currently rendered image or null
+ */
+ @Nullable
+ BufferedImage getAwtImage() {
+ BufferedImage awtImage = mAwtImage.get();
+ if (awtImage == null && mImage != null) {
+ awtImage = SwtUtils.convertToAwt(mImage);
+ }
+
+ return awtImage;
+ }
+
+ /**
+ * Returns whether this image overlay should be painted with a drop shadow.
+ * This is usually the case, but not for transparent themes like the dialog
+ * theme (Theme.*Dialog), which already provides its own shadow.
+ *
+ * @return true if the image overlay should be shown with a drop shadow.
+ */
+ public boolean getShowDropShadow() {
+ return mShowDropShadow;
+ }
+
+ @Override
+ public synchronized void paint(GC gc) {
+ if (mImage != null) {
+ boolean valid = mCanvas.getViewHierarchy().isValid();
+ mCanvas.ensureZoomed();
+ if (!valid) {
+ gc_setAlpha(gc, 128); // half-transparent
+ }
+
+ CanvasTransform hi = mHScale;
+ CanvasTransform vi = mVScale;
+
+ // On some platforms, dynamic image scaling is very slow (see issue #19447) so
+ // compute a pre-scaled version of the image once and render that instead.
+ // This is done lazily in paint rather than when the image changes because
+ // the image must be rescaled each time the zoom level changes, which varies
+ // independently from when the image changes.
+ BufferedImage awtImage = mAwtImage.get();
+ if (PRESCALE && awtImage != null) {
+ int imageWidth = (mPreScaledImage == null) ? 0
+ : mPreScaledImage.getImageData().width
+ - (mShowDropShadow ? SHADOW_SIZE : 0);
+ if (mPreScaledImage == null || imageWidth != hi.getScaledImgSize()) {
+ double xScale = hi.getScaledImgSize() / (double) awtImage.getWidth();
+ double yScale = vi.getScaledImgSize() / (double) awtImage.getHeight();
+ BufferedImage scaledAwtImage;
+
+ // NOTE: == comparison on floating point numbers is okay
+ // here because we normalize the scaling factor
+ // to an exact 1.0 in the zooming code when the value gets
+ // near 1.0 to make painting more efficient in the presence
+ // of rounding errors.
+ if (xScale == 1.0 && yScale == 1.0) {
+ // Scaling to 100% is easy!
+ scaledAwtImage = awtImage;
+
+ if (mShowDropShadow) {
+ // Just need to draw drop shadows
+ scaledAwtImage = ImageUtils.createRectangularDropShadow(awtImage);
+ }
+ } else {
+ if (mShowDropShadow) {
+ scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale,
+ SHADOW_SIZE, SHADOW_SIZE);
+ ImageUtils.drawRectangleShadow(scaledAwtImage, 0, 0,
+ scaledAwtImage.getWidth() - SHADOW_SIZE,
+ scaledAwtImage.getHeight() - SHADOW_SIZE);
+ } else {
+ scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale);
+ }
+ }
+
+ if (mPreScaledImage != null && !mPreScaledImage.isDisposed()) {
+ mPreScaledImage.dispose();
+ }
+ mPreScaledImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), scaledAwtImage,
+ true /*transferAlpha*/, -1);
+ // We can't just clear the mAwtImageStrongRef here, because if the
+ // zooming factor changes, we may need to use it again
+ }
+
+ if (mPreScaledImage != null) {
+ gc.drawImage(mPreScaledImage, hi.translate(0), vi.translate(0));
+ }
+ return;
+ }
+
+ // we only anti-alias when reducing the image size.
+ int oldAlias = -2;
+ if (hi.getScale() < 1.0) {
+ oldAlias = gc_setAntialias(gc, SWT.ON);
+ }
+
+ int srcX = 0;
+ int srcY = 0;
+ int srcWidth = hi.getImgSize();
+ int srcHeight = vi.getImgSize();
+ int destX = hi.translate(0);
+ int destY = vi.translate(0);
+ int destWidth = hi.getScaledImgSize();
+ int destHeight = vi.getScaledImgSize();
+
+ gc.drawImage(mImage,
+ srcX, srcY, srcWidth, srcHeight,
+ destX, destY, destWidth, destHeight);
+
+ if (mShowDropShadow) {
+ SwtUtils.drawRectangleShadow(gc, destX, destY, destWidth, destHeight);
+ }
+
+ if (oldAlias != -2) {
+ gc_setAntialias(gc, oldAlias);
+ }
+
+ if (!valid) {
+ gc_setAlpha(gc, 255); // opaque
+ }
+ }
+ }
+
+ /**
+ * Sets the alpha for the given GC.
+ * <p/>
+ * Alpha may not work on all platforms and may fail with an exception, which
+ * is hidden here (false is returned in that case).
+ *
+ * @param gc the GC to change
+ * @param alpha the new alpha, 0 for transparent, 255 for opaque.
+ * @return True if the operation worked, false if it failed with an
+ * exception.
+ * @see GC#setAlpha(int)
+ */
+ private boolean gc_setAlpha(GC gc, int alpha) {
+ try {
+ gc.setAlpha(alpha);
+ return true;
+ } catch (SWTException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets the non-text antialias flag for the given GC.
+ * <p/>
+ * Antialias may not work on all platforms and may fail with an exception,
+ * which is hidden here (-2 is returned in that case).
+ *
+ * @param gc the GC to change
+ * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
+ * @return The previous aliasing mode if the operation worked, or -2 if it
+ * failed with an exception.
+ * @see GC#setAntialias(int)
+ */
+ private int gc_setAntialias(GC gc, int alias) {
+ try {
+ int old = gc.getAntialias();
+ gc.setAntialias(alias);
+ return old;
+ } catch (SWTException e) {
+ return -2;
+ }
+ }
+
+ /**
+ * Custom {@link BufferedImage} class able to convert itself into an SWT {@link Image}
+ * efficiently.
+ *
+ * The BufferedImage also contains an instance of {@link ImageData} that's kept around
+ * and used to create new SWT {@link Image} objects in {@link #getSwtImage()}.
+ *
+ */
+ private static final class SwtReadyBufferedImage extends BufferedImage {
+
+ private final ImageData mImageData;
+ private final Device mDevice;
+
+ /**
+ * Creates the image with a given model, raster and SWT {@link ImageData}
+ * @param model the color model
+ * @param raster the image raster
+ * @param imageData the SWT image data.
+ * @param device the {@link Device} in which the SWT image will be painted.
+ */
+ private SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device) {
+ super(width, height, BufferedImage.TYPE_INT_ARGB);
+ mImageData = imageData;
+ mDevice = device;
+ }
+
+ /**
+ * Returns a new {@link Image} object initialized with the content of the BufferedImage.
+ * @return the image object.
+ */
+ private Image getSwtImage() {
+ // transfer the content of the bufferedImage into the image data.
+ WritableRaster raster = getRaster();
+ int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData();
+
+ mImageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+
+ return new Image(mDevice, mImageData);
+ }
+
+ /**
+ * Creates a new {@link SwtReadyBufferedImage}.
+ * @param w the width of the image
+ * @param h the height of the image
+ * @param device the device in which the SWT image will be painted
+ * @return a new {@link SwtReadyBufferedImage} object
+ */
+ private static SwtReadyBufferedImage createImage(int w, int h, Device device) {
+ // NOTE: We can't make this image bigger to accommodate the drop shadow directly
+ // (such that we could paint one into the image after a layoutlib render)
+ // since this image is in the full resolution of the device, and gets scaled
+ // to fit in the layout editor. This would have the net effect of causing
+ // the drop shadow to get zoomed/scaled along with the scene, making a tiny
+ // drop shadow for tablet layouts, a huge drop shadow for tiny QVGA screens, etc.
+
+ ImageData imageData = new ImageData(w, h, 32,
+ new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
+
+ SwtReadyBufferedImage swtReadyImage = new SwtReadyBufferedImage(w, h,
+ imageData, device);
+
+ return swtReadyImage;
+ }
+ }
+
+ /**
+ * Implementation of {@link IImageFactory#getImage(int, int)}.
+ */
+ @Override
+ public BufferedImage getImage(int w, int h) {
+ BufferedImage awtImage = mAwtImage.get();
+ if (awtImage == null ||
+ awtImage.getWidth() != w ||
+ awtImage.getHeight() != h) {
+ mAwtImage.clear();
+ awtImage = SwtReadyBufferedImage.createImage(w, h, getDevice());
+ mAwtImage = new SoftReference<BufferedImage>(awtImage);
+ if (PRESCALE) {
+ mAwtImageStrongRef = awtImage;
+ }
+ }
+
+ return awtImage;
+ }
+
+ /**
+ * Returns the bounds of the current image, or null
+ *
+ * @return the bounds of the current image, or null
+ */
+ public Rect getImageBounds() {
+ if (mImage == null) {
+ return null;
+ }
+
+ return new Rect(0, 0, mImage.getImageData().width, mImage.getImageData().height);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
new file mode 100644
index 000000000..b5bc9aa72
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
@@ -0,0 +1,979 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.DOT_9PNG;
+import static com.android.SdkConstants.DOT_BMP;
+import static com.android.SdkConstants.DOT_GIF;
+import static com.android.SdkConstants.DOT_JPG;
+import static com.android.SdkConstants.DOT_PNG;
+import static com.android.utils.SdkUtils.endsWithIgnoreCase;
+import static java.awt.RenderingHints.KEY_ANTIALIASING;
+import static java.awt.RenderingHints.KEY_INTERPOLATION;
+import static java.awt.RenderingHints.KEY_RENDERING;
+import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
+import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR;
+import static java.awt.RenderingHints.VALUE_RENDER_QUALITY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Utilities related to image processing.
+ */
+public class ImageUtils {
+ /**
+ * Returns true if the given image has no dark pixels
+ *
+ * @param image the image to be checked for dark pixels
+ * @return true if no dark pixels were found
+ */
+ public static boolean containsDarkPixels(BufferedImage image) {
+ for (int y = 0, height = image.getHeight(); y < height; y++) {
+ for (int x = 0, width = image.getWidth(); x < width; x++) {
+ int pixel = image.getRGB(x, y);
+ if ((pixel & 0xFF000000) != 0) {
+ int r = (pixel & 0xFF0000) >> 16;
+ int g = (pixel & 0x00FF00) >> 8;
+ int b = (pixel & 0x0000FF);
+
+ // One perceived luminance formula is (0.299*red + 0.587*green + 0.114*blue)
+ // In order to keep this fast since we don't need a very accurate
+ // measure, I'll just estimate this with integer math:
+ long brightness = (299L*r + 587*g + 114*b) / 1000;
+ if (brightness < 128) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the perceived brightness of the given RGB integer on a scale from 0 to 255
+ *
+ * @param rgb the RGB triplet, 8 bits each
+ * @return the perceived brightness, with 0 maximally dark and 255 maximally bright
+ */
+ public static int getBrightness(int rgb) {
+ if ((rgb & 0xFFFFFF) != 0) {
+ int r = (rgb & 0xFF0000) >> 16;
+ int g = (rgb & 0x00FF00) >> 8;
+ int b = (rgb & 0x0000FF);
+ // See the containsDarkPixels implementation for details
+ return (int) ((299L*r + 587*g + 114*b) / 1000);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Converts an alpha-red-green-blue integer color into an {@link RGB} color.
+ * <p>
+ * <b>NOTE</b> - this will drop the alpha value since {@link RGB} objects do not
+ * contain transparency information.
+ *
+ * @param rgb the RGB integer to convert to a color description
+ * @return the color description corresponding to the integer
+ */
+ public static RGB intToRgb(int rgb) {
+ return new RGB((rgb & 0xFF0000) >>> 16, (rgb & 0xFF00) >>> 8, rgb & 0xFF);
+ }
+
+ /**
+ * Converts an {@link RGB} color into a alpha-red-green-blue integer
+ *
+ * @param rgb the RGB color descriptor to convert
+ * @param alpha the amount of alpha to add into the color integer (since the
+ * {@link RGB} objects do not contain an alpha channel)
+ * @return an integer corresponding to the {@link RGB} color
+ */
+ public static int rgbToInt(RGB rgb, int alpha) {
+ return alpha << 24 | (rgb.red << 16) | (rgb.green << 8) | rgb.blue;
+ }
+
+ /**
+ * Crops blank pixels from the edges of the image and returns the cropped result. We
+ * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
+ * this is not the same as pixels that aren't opaque (an alpha value other than 255).
+ *
+ * @param image the image to be cropped
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ @Nullable
+ public static BufferedImage cropBlank(
+ @NonNull BufferedImage image,
+ @Nullable Rect initialCrop) {
+ return cropBlank(image, initialCrop, image.getType());
+ }
+
+ /**
+ * Crops blank pixels from the edges of the image and returns the cropped result. We
+ * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
+ * this is not the same as pixels that aren't opaque (an alpha value other than 255).
+ *
+ * @param image the image to be cropped
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @param imageType the type of {@link BufferedImage} to create
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop, int imageType) {
+ CropFilter filter = new CropFilter() {
+ @Override
+ public boolean crop(BufferedImage bufferedImage, int x, int y) {
+ int rgb = bufferedImage.getRGB(x, y);
+ return (rgb & 0xFF000000) == 0x00000000;
+ // TODO: Do a threshold of 80 instead of just 0? Might give better
+ // visual results -- e.g. check <= 0x80000000
+ }
+ };
+ return crop(image, filter, initialCrop, imageType);
+ }
+
+ /**
+ * Crops pixels of a given color from the edges of the image and returns the cropped
+ * result.
+ *
+ * @param image the image to be cropped
+ * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
+ * bits of alpha, red, green and blue
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ @Nullable
+ public static BufferedImage cropColor(
+ @NonNull BufferedImage image,
+ final int blankArgb,
+ @Nullable Rect initialCrop) {
+ return cropColor(image, blankArgb, initialCrop, image.getType());
+ }
+
+ /**
+ * Crops pixels of a given color from the edges of the image and returns the cropped
+ * result.
+ *
+ * @param image the image to be cropped
+ * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
+ * bits of alpha, red, green and blue
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @param imageType the type of {@link BufferedImage} to create
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ public static BufferedImage cropColor(BufferedImage image,
+ final int blankArgb, Rect initialCrop, int imageType) {
+ CropFilter filter = new CropFilter() {
+ @Override
+ public boolean crop(BufferedImage bufferedImage, int x, int y) {
+ return blankArgb == bufferedImage.getRGB(x, y);
+ }
+ };
+ return crop(image, filter, initialCrop, imageType);
+ }
+
+ /**
+ * Interface implemented by cropping functions that determine whether
+ * a pixel should be cropped or not.
+ */
+ private static interface CropFilter {
+ /**
+ * Returns true if the pixel is should be cropped.
+ *
+ * @param image the image containing the pixel in question
+ * @param x the x position of the pixel
+ * @param y the y position of the pixel
+ * @return true if the pixel should be cropped (for example, is blank)
+ */
+ boolean crop(BufferedImage image, int x, int y);
+ }
+
+ private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop,
+ int imageType) {
+ if (image == null) {
+ return null;
+ }
+
+ // First, determine the dimensions of the real image within the image
+ int x1, y1, x2, y2;
+ if (initialCrop != null) {
+ x1 = initialCrop.x;
+ y1 = initialCrop.y;
+ x2 = initialCrop.x + initialCrop.w;
+ y2 = initialCrop.y + initialCrop.h;
+ } else {
+ x1 = 0;
+ y1 = 0;
+ x2 = image.getWidth();
+ y2 = image.getHeight();
+ }
+
+ // Nothing left to crop
+ if (x1 == x2 || y1 == y2) {
+ return null;
+ }
+
+ // This algorithm is a bit dumb -- it just scans along the edges looking for
+ // a pixel that shouldn't be cropped. I could maybe try to make it smarter by
+ // for example doing a binary search to quickly eliminate large empty areas to
+ // the right and bottom -- but this is slightly tricky with components like the
+ // AnalogClock where I could accidentally end up finding a blank horizontal or
+ // vertical line somewhere in the middle of the rendering of the clock, so for now
+ // we do the dumb thing -- not a big deal since we tend to crop reasonably
+ // small images.
+
+ // First determine top edge
+ topEdge: for (; y1 < y2; y1++) {
+ for (int x = x1; x < x2; x++) {
+ if (!filter.crop(image, x, y1)) {
+ break topEdge;
+ }
+ }
+ }
+
+ if (y1 == image.getHeight()) {
+ // The image is blank
+ return null;
+ }
+
+ // Next determine left edge
+ leftEdge: for (; x1 < x2; x1++) {
+ for (int y = y1; y < y2; y++) {
+ if (!filter.crop(image, x1, y)) {
+ break leftEdge;
+ }
+ }
+ }
+
+ // Next determine right edge
+ rightEdge: for (; x2 > x1; x2--) {
+ for (int y = y1; y < y2; y++) {
+ if (!filter.crop(image, x2 - 1, y)) {
+ break rightEdge;
+ }
+ }
+ }
+
+ // Finally determine bottom edge
+ bottomEdge: for (; y2 > y1; y2--) {
+ for (int x = x1; x < x2; x++) {
+ if (!filter.crop(image, x, y2 - 1)) {
+ break bottomEdge;
+ }
+ }
+ }
+
+ // No need to crop?
+ if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) {
+ return image;
+ }
+
+ if (x1 == x2 || y1 == y2) {
+ // Nothing left after crop -- blank image
+ return null;
+ }
+
+ int width = x2 - x1;
+ int height = y2 - y1;
+
+ // Now extract the sub-image
+ if (imageType == -1) {
+ imageType = image.getType();
+ }
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ BufferedImage cropped = new BufferedImage(width, height, imageType);
+ Graphics g = cropped.getGraphics();
+ g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null);
+
+ g.dispose();
+
+ return cropped;
+ }
+
+ /**
+ * Creates a drop shadow of a given image and returns a new image which shows the
+ * input image on top of its drop shadow.
+ * <p>
+ * <b>NOTE: If the shape is rectangular and opaque, consider using
+ * {@link #drawRectangleShadow(Graphics, int, int, int, int)} instead.</b>
+ *
+ * @param source the source image to be shadowed
+ * @param shadowSize the size of the shadow in pixels
+ * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
+ * @param shadowRgb the RGB int to use for the shadow color
+ * @return a new image with the source image on top of its shadow
+ */
+ public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
+ float shadowOpacity, int shadowRgb) {
+
+ // This code is based on
+ // http://www.jroller.com/gfx/entry/non_rectangular_shadow
+
+ BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2,
+ source.getHeight() + shadowSize * 2,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = image.createGraphics();
+ g2.drawImage(source, null, shadowSize, shadowSize);
+
+ int dstWidth = image.getWidth();
+ int dstHeight = image.getHeight();
+
+ int left = (shadowSize - 1) >> 1;
+ int right = shadowSize - left;
+ int xStart = left;
+ int xStop = dstWidth - right;
+ int yStart = left;
+ int yStop = dstHeight - right;
+
+ shadowRgb = shadowRgb & 0x00FFFFFF;
+
+ int[] aHistory = new int[shadowSize];
+ int historyIdx = 0;
+
+ int aSum;
+
+ int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+ int lastPixelOffset = right * dstWidth;
+ float sumDivider = shadowOpacity / shadowSize;
+
+ // horizontal pass
+ for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
+ aSum = 0;
+ historyIdx = 0;
+ for (int x = 0; x < shadowSize; x++, bufferOffset++) {
+ int a = dataBuffer[bufferOffset] >>> 24;
+ aHistory[x] = a;
+ aSum += a;
+ }
+
+ bufferOffset -= right;
+
+ for (int x = xStart; x < xStop; x++, bufferOffset++) {
+ int a = (int) (aSum * sumDivider);
+ dataBuffer[bufferOffset] = a << 24 | shadowRgb;
+
+ // subtract the oldest pixel from the sum
+ aSum -= aHistory[historyIdx];
+
+ // get the latest pixel
+ a = dataBuffer[bufferOffset + right] >>> 24;
+ aHistory[historyIdx] = a;
+ aSum += a;
+
+ if (++historyIdx >= shadowSize) {
+ historyIdx -= shadowSize;
+ }
+ }
+ }
+ // vertical pass
+ for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
+ aSum = 0;
+ historyIdx = 0;
+ for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
+ int a = dataBuffer[bufferOffset] >>> 24;
+ aHistory[y] = a;
+ aSum += a;
+ }
+
+ bufferOffset -= lastPixelOffset;
+
+ for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
+ int a = (int) (aSum * sumDivider);
+ dataBuffer[bufferOffset] = a << 24 | shadowRgb;
+
+ // subtract the oldest pixel from the sum
+ aSum -= aHistory[historyIdx];
+
+ // get the latest pixel
+ a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
+ aHistory[historyIdx] = a;
+ aSum += a;
+
+ if (++historyIdx >= shadowSize) {
+ historyIdx -= shadowSize;
+ }
+ }
+ }
+
+ g2.drawImage(source, null, 0, 0);
+ g2.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by
+ * {@link #SHADOW_SIZE} around the given source and returns a new image with
+ * both combined
+ *
+ * @param source the source image
+ * @return the source image with a drop shadow on the bottom and right
+ */
+ public static BufferedImage createRectangularDropShadow(BufferedImage source) {
+ int type = source.getType();
+ if (type == BufferedImage.TYPE_CUSTOM) {
+ type = BufferedImage.TYPE_INT_ARGB;
+ }
+
+ int width = source.getWidth();
+ int height = source.getHeight();
+ BufferedImage image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
+ Graphics g = image.getGraphics();
+ g.drawImage(source, 0, 0, width, height, null);
+ ImageUtils.drawRectangleShadow(image, 0, 0, width, height);
+ g.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * The size of the shadow is {@link #SHADOW_SIZE}.
+ *
+ * @param image the image to draw the shadow into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawRectangleShadow(BufferedImage image,
+ int x, int y, int width, int height) {
+ Graphics gc = image.getGraphics();
+ try {
+ drawRectangleShadow(gc, x, y, width, height);
+ } finally {
+ gc.dispose();
+ }
+ }
+
+ /**
+ * Draws a small drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
+ *
+ * @param image the image to draw the shadow into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawSmallRectangleShadow(BufferedImage image,
+ int x, int y, int width, int height) {
+ Graphics gc = image.getGraphics();
+ try {
+ drawSmallRectangleShadow(gc, x, y, width, height);
+ } finally {
+ gc.dispose();
+ }
+ }
+
+ /**
+ * The width and height of the drop shadow painted by
+ * {@link #drawRectangleShadow(Graphics, int, int, int, int)}
+ */
+ public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
+
+ /**
+ * The width and height of the drop shadow painted by
+ * {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)}
+ */
+ public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * <p>
+ * This corresponds to
+ * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)},
+ * but applied to an AWT graphics object instead, such that no image
+ * conversion has to be performed.
+ * <p>
+ * Make sure to keep changes in the visual appearance here in sync with the
+ * AWT version in
+ * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)}.
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawRectangleShadow(Graphics gc,
+ int x, int y, int width, int height) {
+ if (sShadowBottomLeft == null) {
+ // Shadow graphics. This was generated by creating a drop shadow in
+ // Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
+ // color=black, and opacity=51. These values attempt to make a shadow
+ // that is legible both for dark and light themes, on top of the
+ // canvas background (rgb(150,150,150). Darker shadows would tend to
+ // blend into the foreground for a dark holo screen, and lighter shadows
+ // would be hard to spot on the canvas background. If you make adjustments,
+ // make sure to check the shadow with both dark and light themes.
+ //
+ // After making the graphics, I cut out the top right, bottom left
+ // and bottom right corners as 20x20 images, and these are reproduced by
+ // painting them in the corresponding places in the target graphics context.
+ // I then grabbed a single horizontal gradient line from the middle of the
+ // right edge,and a single vertical gradient line from the bottom. These
+ // are then painted scaled/stretched in the target to fill the gaps between
+ // the three corner images.
+ //
+ // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
+ sShadowBottomLeft = readImage("shadow-bl.png"); //$NON-NLS-1$
+ sShadowBottom = readImage("shadow-b.png"); //$NON-NLS-1$
+ sShadowBottomRight = readImage("shadow-br.png"); //$NON-NLS-1$
+ sShadowRight = readImage("shadow-r.png"); //$NON-NLS-1$
+ sShadowTopRight = readImage("shadow-tr.png"); //$NON-NLS-1$
+ assert sShadowBottomLeft != null;
+ assert sShadowBottomRight.getWidth() == SHADOW_SIZE;
+ assert sShadowBottomRight.getHeight() == SHADOW_SIZE;
+ }
+
+ int blWidth = sShadowBottomLeft.getWidth();
+ int trHeight = sShadowTopRight.getHeight();
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(sShadowBottomLeft, x, y + height, null);
+ gc.drawImage(sShadowBottomRight, x + width, y + height, null);
+ gc.drawImage(sShadowTopRight, x + width, y, null);
+ gc.drawImage(sShadowBottom,
+ x + sShadowBottomLeft.getWidth(), y + height,
+ x + width, y + height + sShadowBottom.getHeight(),
+ 0, 0, sShadowBottom.getWidth(), sShadowBottom.getHeight(),
+ null);
+ gc.drawImage(sShadowRight,
+ x + width, y + sShadowTopRight.getHeight(),
+ x + width + sShadowRight.getWidth(), y + height,
+ 0, 0, sShadowRight.getWidth(), sShadowRight.getHeight(),
+ null);
+ }
+
+ /**
+ * Draws a small drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * <p>
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawSmallRectangleShadow(Graphics gc,
+ int x, int y, int width, int height) {
+ if (sShadow2BottomLeft == null) {
+ // Shadow graphics. This was generated by creating a drop shadow in
+ // Gimp, using the parameters x offset=5, y offset=%, blur radius=5,
+ // color=black, and opacity=51. These values attempt to make a shadow
+ // that is legible both for dark and light themes, on top of the
+ // canvas background (rgb(150,150,150). Darker shadows would tend to
+ // blend into the foreground for a dark holo screen, and lighter shadows
+ // would be hard to spot on the canvas background. If you make adjustments,
+ // make sure to check the shadow with both dark and light themes.
+ //
+ // After making the graphics, I cut out the top right, bottom left
+ // and bottom right corners as 20x20 images, and these are reproduced by
+ // painting them in the corresponding places in the target graphics context.
+ // I then grabbed a single horizontal gradient line from the middle of the
+ // right edge,and a single vertical gradient line from the bottom. These
+ // are then painted scaled/stretched in the target to fill the gaps between
+ // the three corner images.
+ //
+ // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
+ sShadow2BottomLeft = readImage("shadow2-bl.png"); //$NON-NLS-1$
+ sShadow2Bottom = readImage("shadow2-b.png"); //$NON-NLS-1$
+ sShadow2BottomRight = readImage("shadow2-br.png"); //$NON-NLS-1$
+ sShadow2Right = readImage("shadow2-r.png"); //$NON-NLS-1$
+ sShadow2TopRight = readImage("shadow2-tr.png"); //$NON-NLS-1$
+ assert sShadow2BottomLeft != null;
+ assert sShadow2TopRight != null;
+ assert sShadow2BottomRight.getWidth() == SMALL_SHADOW_SIZE;
+ assert sShadow2BottomRight.getHeight() == SMALL_SHADOW_SIZE;
+ }
+
+ int blWidth = sShadow2BottomLeft.getWidth();
+ int trHeight = sShadow2TopRight.getHeight();
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(sShadow2BottomLeft, x, y + height, null);
+ gc.drawImage(sShadow2BottomRight, x + width, y + height, null);
+ gc.drawImage(sShadow2TopRight, x + width, y, null);
+ gc.drawImage(sShadow2Bottom,
+ x + sShadow2BottomLeft.getWidth(), y + height,
+ x + width, y + height + sShadow2Bottom.getHeight(),
+ 0, 0, sShadow2Bottom.getWidth(), sShadow2Bottom.getHeight(),
+ null);
+ gc.drawImage(sShadow2Right,
+ x + width, y + sShadow2TopRight.getHeight(),
+ x + width + sShadow2Right.getWidth(), y + height,
+ 0, 0, sShadow2Right.getWidth(), sShadow2Right.getHeight(),
+ null);
+ }
+
+ /**
+ * Reads the given image from the plugin folder
+ *
+ * @param name the name of the image (including file extension)
+ * @return the corresponding image, or null if something goes wrong
+ */
+ @Nullable
+ public static BufferedImage readImage(@NonNull String name) {
+ InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$
+ if (stream != null) {
+ try {
+ return ImageIO.read(stream);
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Could not read %1$s", name);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Dumb API
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // Normal drop shadow
+ private static BufferedImage sShadowBottomLeft;
+ private static BufferedImage sShadowBottom;
+ private static BufferedImage sShadowBottomRight;
+ private static BufferedImage sShadowRight;
+ private static BufferedImage sShadowTopRight;
+
+ // Small drop shadow
+ private static BufferedImage sShadow2BottomLeft;
+ private static BufferedImage sShadow2Bottom;
+ private static BufferedImage sShadow2BottomRight;
+ private static BufferedImage sShadow2Right;
+ private static BufferedImage sShadow2TopRight;
+
+ /**
+ * Returns a bounding rectangle for the given list of rectangles. If the list is
+ * empty, the bounding rectangle is null.
+ *
+ * @param items the list of rectangles to compute a bounding rectangle for (may not be
+ * null)
+ * @return a bounding rectangle of the passed in rectangles, or null if the list is
+ * empty
+ */
+ public static Rectangle getBoundingRectangle(List<Rectangle> items) {
+ Iterator<Rectangle> iterator = items.iterator();
+ if (!iterator.hasNext()) {
+ return null;
+ }
+
+ Rectangle bounds = iterator.next();
+ Rectangle union = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
+ while (iterator.hasNext()) {
+ union.add(iterator.next());
+ }
+
+ return union;
+ }
+
+ /**
+ * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
+ * to (x2,y2)
+ *
+ * @param source the source image
+ * @param x1 top left X coordinate
+ * @param y1 top left Y coordinate
+ * @param x2 bottom right X coordinate
+ * @param y2 bottom right Y coordinate
+ * @return a new image containing the pixels in the given range
+ */
+ public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
+ int width = x2 - x1;
+ int height = y2 - y1;
+ int imageType = source.getType();
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ BufferedImage sub = new BufferedImage(width, height, imageType);
+ Graphics g = sub.getGraphics();
+ g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
+ g.dispose();
+
+ return sub;
+ }
+
+ /**
+ * Returns the color value represented by the given string value
+ * @param value the color value
+ * @return the color as an int
+ * @throw NumberFormatException if the conversion failed.
+ */
+ public static int getColor(String value) {
+ // Copied from ResourceHelper in layoutlib
+ if (value != null) {
+ if (value.startsWith("#") == false) { //$NON-NLS-1$
+ throw new NumberFormatException(
+ String.format("Color value '%s' must start with #", value));
+ }
+
+ value = value.substring(1);
+
+ // make sure it's not longer than 32bit
+ if (value.length() > 8) {
+ throw new NumberFormatException(String.format(
+ "Color value '%s' is too long. Format is either" +
+ "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
+ value));
+ }
+
+ if (value.length() == 3) { // RGB format
+ char[] color = new char[8];
+ color[0] = color[1] = 'F';
+ color[2] = color[3] = value.charAt(0);
+ color[4] = color[5] = value.charAt(1);
+ color[6] = color[7] = value.charAt(2);
+ value = new String(color);
+ } else if (value.length() == 4) { // ARGB format
+ char[] color = new char[8];
+ color[0] = color[1] = value.charAt(0);
+ color[2] = color[3] = value.charAt(1);
+ color[4] = color[5] = value.charAt(2);
+ color[6] = color[7] = value.charAt(3);
+ value = new String(color);
+ } else if (value.length() == 6) {
+ value = "FF" + value; //$NON-NLS-1$
+ }
+
+ // this is a RRGGBB or AARRGGBB value
+
+ // Integer.parseInt will fail to parse strings like "ff191919", so we use
+ // a Long, but cast the result back into an int, since we know that we're only
+ // dealing with 32 bit values.
+ return (int)Long.parseLong(value, 16);
+ }
+
+ throw new NumberFormatException();
+ }
+
+ /**
+ * Resize the given image
+ *
+ * @param source the image to be scaled
+ * @param xScale x scale
+ * @param yScale y scale
+ * @return the scaled image
+ */
+ public static BufferedImage scale(BufferedImage source, double xScale, double yScale) {
+ return scale(source, xScale, yScale, 0, 0);
+ }
+
+ /**
+ * Resize the given image
+ *
+ * @param source the image to be scaled
+ * @param xScale x scale
+ * @param yScale y scale
+ * @param rightMargin extra margin to add on the right
+ * @param bottomMargin extra margin to add on the bottom
+ * @return the scaled image
+ */
+ public static BufferedImage scale(BufferedImage source, double xScale, double yScale,
+ int rightMargin, int bottomMargin) {
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+ int destWidth = Math.max(1, (int) (xScale * sourceWidth));
+ int destHeight = Math.max(1, (int) (yScale * sourceHeight));
+ int imageType = source.getType();
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ if (xScale > 0.5 && yScale > 0.5) {
+ BufferedImage scaled =
+ new BufferedImage(destWidth + rightMargin, destHeight + bottomMargin, imageType);
+ Graphics2D g2 = scaled.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setColor(new Color(0, true));
+ g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin);
+ g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight,
+ null);
+ g2.dispose();
+ return scaled;
+ } else {
+ // When creating a thumbnail, using the above code doesn't work very well;
+ // you get some visible artifacts, especially for text. Instead use the
+ // technique of repeatedly scaling the image into half; this will cause
+ // proper averaging of neighboring pixels, and will typically (for the kinds
+ // of screen sizes used by this utility method in the layout editor) take
+ // about 3-4 iterations to get the result since we are logarithmically reducing
+ // the size. Besides, each successive pass in operating on much fewer pixels
+ // (a reduction of 4 in each pass).
+ //
+ // However, we may not be resizing to a size that can be reached exactly by
+ // successively diving in half. Therefore, once we're within a factor of 2 of
+ // the final size, we can do a resize to the exact target size.
+ // However, we can get even better results if we perform this final resize
+ // up front. Let's say we're going from width 1000 to a destination width of 85.
+ // The first approach would cause a resize from 1000 to 500 to 250 to 125, and
+ // then a resize from 125 to 85. That last resize can distort/blur a lot.
+ // Instead, we can start with the destination width, 85, and double it
+ // successfully until we're close to the initial size: 85, then 170,
+ // then 340, and finally 680. (The next one, 1360, is larger than 1000).
+ // So, now we *start* the thumbnail operation by resizing from width 1000 to
+ // width 680, which will preserve a lot of visual details such as text.
+ // Then we can successively resize the image in half, 680 to 340 to 170 to 85.
+ // We end up with the expected final size, but we've been doing an exact
+ // divide-in-half resizing operation at the end so there is less distortion.
+
+
+ int iterations = 0; // Number of halving operations to perform after the initial resize
+ int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer
+ int nearestHeight = destHeight;
+ while (nearestWidth < sourceWidth / 2) {
+ nearestWidth *= 2;
+ nearestHeight *= 2;
+ iterations++;
+ }
+
+ // If we're supposed to add in margins, we need to do it in the initial resizing
+ // operation if we don't have any subsequent resizing operations.
+ if (iterations == 0) {
+ nearestWidth += rightMargin;
+ nearestHeight += bottomMargin;
+ }
+
+ BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType);
+ Graphics2D g2 = scaled.createGraphics();
+ g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0, nearestWidth, nearestHeight,
+ 0, 0, sourceWidth, sourceHeight, null);
+ g2.dispose();
+
+ sourceWidth = nearestWidth;
+ sourceHeight = nearestHeight;
+ source = scaled;
+
+ for (int iteration = iterations - 1; iteration >= 0; iteration--) {
+ int halfWidth = sourceWidth / 2;
+ int halfHeight = sourceHeight / 2;
+ if (iteration == 0) { // Last iteration: Add margins in final image
+ scaled = new BufferedImage(halfWidth + rightMargin, halfHeight + bottomMargin,
+ imageType);
+ } else {
+ scaled = new BufferedImage(halfWidth, halfHeight, imageType);
+ }
+ g2 = scaled.createGraphics();
+ g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0,
+ halfWidth, halfHeight, 0, 0,
+ sourceWidth, sourceHeight,
+ null);
+ g2.dispose();
+
+ sourceWidth = halfWidth;
+ sourceHeight = halfHeight;
+ source = scaled;
+ iterations--;
+ }
+ return scaled;
+ }
+ }
+
+ /**
+ * Returns true if the given file path points to an image file recognized by
+ * Android. See http://developer.android.com/guide/appendix/media-formats.html
+ * for details.
+ *
+ * @param path the filename to be tested
+ * @return true if the file represents an image file
+ */
+ public static boolean hasImageExtension(String path) {
+ return endsWithIgnoreCase(path, DOT_PNG)
+ || endsWithIgnoreCase(path, DOT_9PNG)
+ || endsWithIgnoreCase(path, DOT_GIF)
+ || endsWithIgnoreCase(path, DOT_JPG)
+ || endsWithIgnoreCase(path, DOT_BMP);
+ }
+
+ /**
+ * Creates a new image of the given size filled with the given color
+ *
+ * @param width the width of the image
+ * @param height the height of the image
+ * @param color the color of the image
+ * @return a new image of the given size filled with the given color
+ */
+ public static BufferedImage createColoredImage(int width, int height, RGB color) {
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics g = image.getGraphics();
+ g.setColor(new Color(color.red, color.green, color.blue));
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+ g.dispose();
+ return image;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
new file mode 100644
index 000000000..7bab914e5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_LAYOUT;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_LAYOUTS;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+import static com.android.resources.ResourceType.LAYOUT;
+import static org.eclipse.core.resources.IResourceDelta.ADDED;
+import static org.eclipse.core.resources.IResourceDelta.CHANGED;
+import static org.eclipse.core.resources.IResourceDelta.CONTENT;
+import static org.eclipse.core.resources.IResourceDelta.REMOVED;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The include finder finds other XML files that are including a given XML file, and does
+ * so efficiently (caching results across IDE sessions etc).
+ */
+@SuppressWarnings("restriction") // XML model
+public class IncludeFinder {
+ /** Qualified name for the per-project persistent property include-map */
+ private final static QualifiedName CONFIG_INCLUDES = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "includes");//$NON-NLS-1$
+
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link IncludeFinder} for this project
+ */
+ private final static QualifiedName INCLUDE_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "includefinder"); //$NON-NLS-1$
+
+ /** Project that the include finder locates includes for */
+ private final IProject mProject;
+
+ /** Map from a layout resource name to a set of layouts included by the given resource */
+ private Map<String, List<String>> mIncludes = null;
+
+ /**
+ * Reverse map of {@link #mIncludes}; points to other layouts that are including a
+ * given layouts
+ */
+ private Map<String, List<String>> mIncludedBy = null;
+
+ /** Flag set during a refresh; ignore updates when this is true */
+ private static boolean sRefreshing;
+
+ /** Global (cross-project) resource listener */
+ private static ResourceListener sListener;
+
+ /**
+ * Constructs an {@link IncludeFinder} for the given project. Don't use this method;
+ * use the {@link #get} factory method instead.
+ *
+ * @param project project to create an {@link IncludeFinder} for
+ */
+ private IncludeFinder(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link IncludeFinder} for the given project
+ *
+ * @param project the project the finder is associated with
+ * @return an {@link IncludeFinder} for the given project, never null
+ */
+ @NonNull
+ public static IncludeFinder get(IProject project) {
+ IncludeFinder finder = null;
+ try {
+ finder = (IncludeFinder) project.getSessionProperty(INCLUDE_FINDER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (finder == null) {
+ finder = new IncludeFinder(project);
+ try {
+ project.setSessionProperty(INCLUDE_FINDER, finder);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store IncludeFinder");
+ }
+ }
+
+ return finder;
+ }
+
+ /**
+ * Returns a list of resource names that are included by the given resource
+ *
+ * @param includer the resource name to return included layouts for
+ * @return the layouts included by the given resource
+ */
+ private List<String> getIncludesFrom(String includer) {
+ ensureInitialized();
+
+ return mIncludes.get(includer);
+ }
+
+ /**
+ * Gets the list of all other layouts that are including the given layout.
+ *
+ * @param included the file that is included
+ * @return the files that are including the given file, or null or empty
+ */
+ @Nullable
+ public List<Reference> getIncludedBy(IResource included) {
+ ensureInitialized();
+ String mapKey = getMapKey(included);
+ List<String> result = mIncludedBy.get(mapKey);
+ if (result == null) {
+ String name = getResourceName(included);
+ if (!name.equals(mapKey)) {
+ result = mIncludedBy.get(name);
+ }
+ }
+
+ if (result != null && result.size() > 0) {
+ List<Reference> references = new ArrayList<Reference>(result.size());
+ for (String s : result) {
+ references.add(new Reference(mProject, s));
+ }
+ return references;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if the given resource is included from some other layout in the
+ * project
+ *
+ * @param included the resource to check
+ * @return true if the file is included by some other layout
+ */
+ public boolean isIncluded(IResource included) {
+ ensureInitialized();
+ String mapKey = getMapKey(included);
+ List<String> result = mIncludedBy.get(mapKey);
+ if (result == null) {
+ String name = getResourceName(included);
+ if (!name.equals(mapKey)) {
+ result = mIncludedBy.get(name);
+ }
+ }
+
+ return result != null && result.size() > 0;
+ }
+
+ @VisibleForTesting
+ /* package */ List<String> getIncludedBy(String included) {
+ ensureInitialized();
+ return mIncludedBy.get(included);
+ }
+
+ /** Initialize the inclusion data structures, if not already done */
+ private void ensureInitialized() {
+ if (mIncludes == null) {
+ // Initialize
+ if (!readSettings()) {
+ // Couldn't read settings: probably the first time this code is running
+ // so there is no known data about includes.
+
+ // Yes, these should be multimaps! If we start using Guava replace
+ // these with multimaps.
+ mIncludes = new HashMap<String, List<String>>();
+ mIncludedBy = new HashMap<String, List<String>>();
+
+ scanProject();
+ saveSettings();
+ }
+ }
+ }
+
+ // ----- Persistence -----
+
+ /**
+ * Create a String serialization of the includes map. The map attempts to be compact;
+ * it strips out the @layout/ prefix, and eliminates the values for empty string
+ * values. The map can be restored by calling {@link #decodeMap}. The encoded String
+ * will have sorted keys.
+ *
+ * @param map the map to be serialized
+ * @return a serialization (never null) of the given map
+ */
+ @VisibleForTesting
+ public static String encodeMap(Map<String, List<String>> map) {
+ StringBuilder sb = new StringBuilder();
+
+ if (map != null) {
+ // Process the keys in sorted order rather than just
+ // iterating over the entry set to ensure stable output
+ List<String> keys = new ArrayList<String>(map.keySet());
+ Collections.sort(keys);
+ for (String key : keys) {
+ List<String> values = map.get(key);
+
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ sb.append(key);
+ if (values.size() > 0) {
+ sb.append('=').append('>');
+ sb.append('{');
+ boolean first = true;
+ for (String value : values) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(',');
+ }
+ sb.append(value);
+ }
+ sb.append('}');
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Decodes the encoding (produced by {@link #encodeMap}) back into the original map,
+ * modulo any key sorting differences.
+ *
+ * @param encoded an encoding of a map created by {@link #encodeMap}
+ * @return a map corresponding to the encoded values, never null
+ */
+ @VisibleForTesting
+ public static Map<String, List<String>> decodeMap(String encoded) {
+ HashMap<String, List<String>> map = new HashMap<String, List<String>>();
+
+ if (encoded.length() > 0) {
+ int i = 0;
+ int end = encoded.length();
+
+ while (i < end) {
+
+ // Find key range
+ int keyBegin = i;
+ int keyEnd = i;
+ while (i < end) {
+ char c = encoded.charAt(i);
+ if (c == ',') {
+ break;
+ } else if (c == '=') {
+ i += 2; // Skip =>
+ break;
+ }
+ i++;
+ keyEnd = i;
+ }
+
+ List<String> values = new ArrayList<String>();
+ // Find values
+ if (i < end && encoded.charAt(i) == '{') {
+ i++;
+ while (i < end) {
+ int valueBegin = i;
+ int valueEnd = i;
+ char c = 0;
+ while (i < end) {
+ c = encoded.charAt(i);
+ if (c == ',' || c == '}') {
+ valueEnd = i;
+ break;
+ }
+ i++;
+ }
+ if (valueEnd > valueBegin) {
+ values.add(encoded.substring(valueBegin, valueEnd));
+ }
+
+ if (c == '}') {
+ if (i < end-1 && encoded.charAt(i+1) == ',') {
+ i++;
+ }
+ break;
+ }
+ assert c == ',';
+ i++;
+ }
+ }
+
+ String key = encoded.substring(keyBegin, keyEnd);
+ map.put(key, values);
+ i++;
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Stores the settings in the persistent project storage.
+ */
+ private void saveSettings() {
+ // Serialize the mIncludes map into a compact String. The mIncludedBy map can be
+ // inferred from it.
+ String encoded = encodeMap(mIncludes);
+
+ try {
+ if (encoded.length() >= 2048) {
+ // The maximum length of a setting key is 2KB, according to the javadoc
+ // for the project class. It's unlikely that we'll
+ // hit this -- even with an average layout root name of 20 characters
+ // we can still store over a hundred names. But JUST IN CASE we run
+ // into this, we'll clear out the key in this name which means that the
+ // information will need to be recomputed in the next IDE session.
+ mProject.setPersistentProperty(CONFIG_INCLUDES, null);
+ } else {
+ String existing = mProject.getPersistentProperty(CONFIG_INCLUDES);
+ if (!encoded.equals(existing)) {
+ mProject.setPersistentProperty(CONFIG_INCLUDES, encoded);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store include settings");
+ }
+ }
+
+ /**
+ * Reads previously stored settings from the persistent project storage
+ *
+ * @return true iff settings were restored from the project
+ */
+ private boolean readSettings() {
+ try {
+ String encoded = mProject.getPersistentProperty(CONFIG_INCLUDES);
+ if (encoded != null) {
+ mIncludes = decodeMap(encoded);
+
+ // Set up a reverse map, pointing from included files to the files that
+ // included them
+ mIncludedBy = new HashMap<String, List<String>>(2 * mIncludes.size());
+ for (Map.Entry<String, List<String>> entry : mIncludes.entrySet()) {
+ // File containing the <include>
+ String includer = entry.getKey();
+ // Files being <include>'ed by the above file
+ List<String> included = entry.getValue();
+ setIncludedBy(includer, included);
+ }
+
+ return true;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't read include settings");
+ }
+
+ return false;
+ }
+
+ // ----- File scanning -----
+
+ /**
+ * Scan the whole project for XML layout resources that are performing includes.
+ */
+ private void scanProject() {
+ ProjectResources resources = ResourceManager.getInstance().getProjectResources(mProject);
+ if (resources != null) {
+ Collection<ResourceItem> layouts = resources.getResourceItemsOfType(LAYOUT);
+ for (ResourceItem layout : layouts) {
+ List<ResourceFile> sources = layout.getSourceFileList();
+ for (ResourceFile source : sources) {
+ updateFileIncludes(source, false);
+ }
+ }
+
+ return;
+ }
+ }
+
+ /**
+ * Scans the given {@link ResourceFile} and if it is a layout resource, updates the
+ * includes in it.
+ *
+ * @param resourceFile the {@link ResourceFile} to be scanned for includes (doesn't
+ * have to be only layout XML files; this method will filter the type)
+ * @param singleUpdate true if this is a single file being updated, false otherwise
+ * (e.g. during initial project scanning)
+ * @return true if we updated the includes for the resource file
+ */
+ private boolean updateFileIncludes(ResourceFile resourceFile, boolean singleUpdate) {
+ Collection<ResourceType> resourceTypes = resourceFile.getResourceTypes();
+ for (ResourceType type : resourceTypes) {
+ if (type == ResourceType.LAYOUT) {
+ ensureInitialized();
+
+ List<String> includes = Collections.emptyList();
+ if (resourceFile.getFile() instanceof IFileWrapper) {
+ IFile file = ((IFileWrapper) resourceFile.getFile()).getIFile();
+
+ // See if we have an existing XML model for this file; if so, we can
+ // just look directly at the parse tree
+ boolean hadXmlModel = false;
+ IStructuredModel model = null;
+ try {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ model = modelManager.getExistingModelForRead(file);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ includes = findIncludesInDocument(document);
+ hadXmlModel = true;
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ // If no XML model we have to read the XML contents and (possibly) parse it.
+ // The actual file may not exist anymore (e.g. when deleting a layout file
+ // or when the workspace is out of sync.)
+ if (!hadXmlModel) {
+ String xml = AdtPlugin.readFile(file);
+ if (xml != null) {
+ includes = findIncludes(xml);
+ }
+ }
+ } else {
+ String xml = AdtPlugin.readFile(resourceFile);
+ if (xml != null) {
+ includes = findIncludes(xml);
+ }
+ }
+
+ String key = getMapKey(resourceFile);
+ if (includes.equals(getIncludesFrom(key))) {
+ // Common case -- so avoid doing settings flush etc
+ return false;
+ }
+
+ boolean detectCycles = singleUpdate;
+ setIncluded(key, includes, detectCycles);
+
+ if (singleUpdate) {
+ saveSettings();
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds the list of includes in the given XML content. It attempts quickly return
+ * empty if the file does not include any include tags; it does this by only parsing
+ * if it detects the string &lt;include in the file.
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<String> findIncludes(@NonNull String xml) {
+ int index = xml.indexOf(ATTR_LAYOUT);
+ if (index != -1) {
+ return findIncludesInXml(xml);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Parses the given XML content and extracts all the included URLs and returns them
+ *
+ * @param xml layout XML content to be parsed for includes
+ * @return a list of included urls, or null
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<String> findIncludesInXml(@NonNull String xml) {
+ Document document = DomUtilities.parseDocument(xml, false /*logParserErrors*/);
+ if (document != null) {
+ return findIncludesInDocument(document);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Searches the given DOM document and returns the list of includes, if any */
+ @NonNull
+ private static List<String> findIncludesInDocument(@NonNull Document document) {
+ List<String> includes = findIncludesInDocument(document, null);
+ if (includes == null) {
+ includes = Collections.emptyList();
+ }
+ return includes;
+ }
+
+ @Nullable
+ private static List<String> findIncludesInDocument(@NonNull Node node,
+ @Nullable List<String> urls) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ String tag = node.getNodeName();
+ boolean isInclude = tag.equals(VIEW_INCLUDE);
+ boolean isFragment = tag.equals(VIEW_FRAGMENT);
+ if (isInclude || isFragment) {
+ Element element = (Element) node;
+ String url;
+ if (isInclude) {
+ url = element.getAttribute(ATTR_LAYOUT);
+ } else {
+ url = element.getAttributeNS(TOOLS_URI, ATTR_LAYOUT);
+ }
+ if (url.length() > 0) {
+ String resourceName = urlToLocalResource(url);
+ if (resourceName != null) {
+ if (urls == null) {
+ urls = new ArrayList<String>();
+ }
+ urls.add(resourceName);
+ }
+ }
+
+ }
+ }
+
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ urls = findIncludesInDocument(children.item(i), urls);
+ }
+
+ return urls;
+ }
+
+
+ /**
+ * Returns the layout URL to a local resource name (provided the URL is a local
+ * resource, not something in @android etc.) Returns null otherwise.
+ */
+ private static String urlToLocalResource(String url) {
+ if (!url.startsWith("@")) { //$NON-NLS-1$
+ return null;
+ }
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd == -1) {
+ return null;
+ }
+ int nameBegin = typeEnd + 1;
+ int typeBegin = 1;
+ int colon = url.lastIndexOf(':', typeEnd);
+ if (colon != -1) {
+ String packageName = url.substring(typeBegin, colon);
+ if ("android".equals(packageName)) { //$NON-NLS-1$
+ // Don't want to point to non-local resources
+ return null;
+ }
+
+ typeBegin = colon + 1;
+ assert "layout".equals(url.substring(typeBegin, typeEnd)); //$NON-NLS-1$
+ }
+
+ return url.substring(nameBegin);
+ }
+
+ /**
+ * Record the list of included layouts from the given layout
+ *
+ * @param includer the layout including other layouts
+ * @param included the layouts that were included by the including layout
+ * @param detectCycles if true, check for cycles and report them as project errors
+ */
+ @VisibleForTesting
+ /* package */ void setIncluded(String includer, List<String> included, boolean detectCycles) {
+ // Remove previously linked inverse mappings
+ List<String> oldIncludes = mIncludes.get(includer);
+ if (oldIncludes != null && oldIncludes.size() > 0) {
+ for (String includee : oldIncludes) {
+ List<String> includers = mIncludedBy.get(includee);
+ if (includers != null) {
+ includers.remove(includer);
+ }
+ }
+ }
+
+ mIncludes.put(includer, included);
+ // Reverse mapping: for included items, point back to including file
+ setIncludedBy(includer, included);
+
+ if (detectCycles) {
+ detectCycles(includer);
+ }
+ }
+
+ /** Record the list of included layouts from the given layout */
+ private void setIncludedBy(String includer, List<String> included) {
+ for (String target : included) {
+ List<String> list = mIncludedBy.get(target);
+ if (list == null) {
+ list = new ArrayList<String>(2); // We don't expect many includes
+ mIncludedBy.put(target, list);
+ }
+ if (!list.contains(includer)) {
+ list.add(includer);
+ }
+ }
+ }
+
+ /** Start listening on project resources */
+ public static void start() {
+ assert sListener == null;
+ sListener = new ResourceListener();
+ ResourceManager.getInstance().addListener(sListener);
+ }
+
+ /** Stop listening on project resources */
+ public static void stop() {
+ assert sListener != null;
+ ResourceManager.getInstance().addListener(sListener);
+ }
+
+ private static String getMapKey(ResourceFile resourceFile) {
+ IAbstractFile file = resourceFile.getFile();
+ String name = file.getName();
+ String folderName = file.getParentFolder().getName();
+ return getMapKey(folderName, name);
+ }
+
+ private static String getMapKey(IResource resourceFile) {
+ String folderName = resourceFile.getParent().getName();
+ String name = resourceFile.getName();
+ return getMapKey(folderName, name);
+ }
+
+ private static String getResourceName(IResource resourceFile) {
+ String name = resourceFile.getName();
+ int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot
+ if (baseEnd > 0) {
+ name = name.substring(0, baseEnd);
+ }
+
+ return name;
+ }
+
+ private static String getMapKey(String folderName, String name) {
+ int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot
+ if (baseEnd > 0) {
+ name = name.substring(0, baseEnd);
+ }
+
+ // Create a map key for the given resource file
+ // This will map
+ // /res/layout/foo.xml => "foo"
+ // /res/layout-land/foo.xml => "-land/foo"
+
+ if (FD_RES_LAYOUT.equals(folderName)) {
+ // Normal case -- keep just the basename
+ return name;
+ } else {
+ // Store the relative path from res/ on down, so
+ // /res/layout-land/foo.xml becomes "layout-land/foo"
+ //if (folderName.startsWith(FD_LAYOUT)) {
+ // folderName = folderName.substring(FD_LAYOUT.length());
+ //}
+
+ return folderName + WS_SEP + name;
+ }
+ }
+
+ /** Listener of resource file saves, used to update layout inclusion data structures */
+ private static class ResourceListener implements IResourceListener {
+ @Override
+ public void fileChanged(IProject project, ResourceFile file, int eventType) {
+ if (sRefreshing) {
+ return;
+ }
+
+ if ((eventType & (CHANGED | ADDED | REMOVED | CONTENT)) == 0) {
+ return;
+ }
+
+ IncludeFinder finder = get(project);
+ if (finder != null) {
+ if (finder.updateFileIncludes(file, true)) {
+ finder.saveSettings();
+ }
+ }
+ }
+
+ @Override
+ public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
+ // We only care about layout resource files
+ }
+ }
+
+ // ----- Cycle detection -----
+
+ private void detectCycles(String from) {
+ // Perform DFS on the include graph and look for a cycle; if we find one, produce
+ // a chain of includes on the way back to show to the user
+ if (mIncludes.size() > 0) {
+ Set<String> visiting = new HashSet<String>(mIncludes.size());
+ String chain = dfs(from, visiting);
+ if (chain != null) {
+ addError(from, chain);
+ } else {
+ // Is there an existing error for us to clean up?
+ removeErrors(from);
+ }
+ }
+ }
+
+ /** Format to chain include cycles in: a=>b=>c=>d etc */
+ private final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$
+
+ private String dfs(String from, Set<String> visiting) {
+ visiting.add(from);
+
+ List<String> includes = mIncludes.get(from);
+ if (includes != null && includes.size() > 0) {
+ for (String include : includes) {
+ if (visiting.contains(include)) {
+ return String.format(CHAIN_FORMAT, from, include);
+ }
+ String chain = dfs(include, visiting);
+ if (chain != null) {
+ return String.format(CHAIN_FORMAT, from, chain);
+ }
+ }
+ }
+
+ visiting.remove(from);
+
+ return null;
+ }
+
+ private void removeErrors(String from) {
+ final IResource resource = findResource(from);
+ if (resource != null) {
+ try {
+ final String markerId = IMarker.PROBLEM;
+
+ IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (final IMarker marker : markers) {
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.startsWith(MESSAGE)) {
+ // Remove
+ runLater(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sRefreshing = true;
+ marker.delete();
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't delete problem marker");
+ } finally {
+ sRefreshing = false;
+ }
+ }
+ });
+ }
+ }
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+ }
+ }
+
+ /** Error message for cycles */
+ private static final String MESSAGE = "Found cyclical <include> chain";
+
+ private void addError(String from, String chain) {
+ final IResource resource = findResource(from);
+ if (resource != null) {
+ final String markerId = IMarker.PROBLEM;
+ final String message = String.format("%1$s: %2$s", MESSAGE, chain);
+ final int lineNumber = 1;
+ final int severity = IMarker.SEVERITY_ERROR;
+
+ // check if there's a similar marker already, since aapt is launched twice
+ boolean markerAlreadyExists = false;
+ try {
+ IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (IMarker marker : markers) {
+ int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (tmpLine != lineNumber) {
+ break;
+ }
+
+ int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (tmpSeverity != severity) {
+ break;
+ }
+
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.equals(message) == false) {
+ break;
+ }
+
+ // if we're here, all the marker attributes are equals, we found it
+ // and exit
+ markerAlreadyExists = true;
+ break;
+ }
+
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+
+ if (!markerAlreadyExists) {
+ runLater(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sRefreshing = true;
+
+ // Adding a resource will force a refresh on the file;
+ // ignore these updates
+ BaseProjectHelper.markResource(resource, markerId, message, lineNumber,
+ severity);
+ } finally {
+ sRefreshing = false;
+ }
+ }
+ });
+ }
+ }
+ }
+
+ // FIXME: Find more standard Eclipse way to do this.
+ // We need to run marker registration/deletion "later", because when the include
+ // scanning is running it's in the middle of resource notification, so the IDE
+ // throws an exception
+ private static void runLater(Runnable runnable) {
+ Display display = Display.findDisplay(Thread.currentThread());
+ if (display != null) {
+ display.asyncExec(runnable);
+ } else {
+ AdtPlugin.log(IStatus.WARNING, "Could not find display");
+ }
+ }
+
+ /**
+ * Finds the project resource for the given layout path
+ *
+ * @param from the resource name
+ * @return the {@link IResource}, or null if not found
+ */
+ private IResource findResource(String from) {
+ final IResource resource = mProject.findMember(WS_LAYOUTS + WS_SEP + from + '.' + EXT_XML);
+ return resource;
+ }
+
+ /**
+ * Creates a blank, project-less {@link IncludeFinder} <b>for use by unit tests
+ * only</b>
+ */
+ @VisibleForTesting
+ /* package */ static IncludeFinder create() {
+ IncludeFinder finder = new IncludeFinder(null);
+ finder.mIncludes = new HashMap<String, List<String>>();
+ finder.mIncludedBy = new HashMap<String, List<String>>();
+ return finder;
+ }
+
+ /** A reference to a particular file in the project */
+ public static class Reference {
+ /** The unique id referencing the file, such as (for res/layout-land/main.xml)
+ * "layout-land/main") */
+ private final String mId;
+
+ /** The project containing the file */
+ private final IProject mProject;
+
+ /** The resource name of the file, such as (for res/layout/main.xml) "main" */
+ private String mName;
+
+ /** Creates a new include reference */
+ private Reference(IProject project, String id) {
+ super();
+ mProject = project;
+ mId = id;
+ }
+
+ /**
+ * Returns the id identifying the given file within the project
+ *
+ * @return the id identifying the given file within the project
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the {@link IFile} in the project for the given file. May return null if
+ * there is an error in locating the file or if the file no longer exists.
+ *
+ * @return the project file, or null
+ */
+ public IFile getFile() {
+ String reference = mId;
+ if (!reference.contains(WS_SEP)) {
+ reference = FD_RES_LAYOUT + WS_SEP + reference;
+ }
+
+ String projectPath = FD_RESOURCES + WS_SEP + reference + '.' + EXT_XML;
+ IResource member = mProject.findMember(projectPath);
+ if (member instanceof IFile) {
+ return (IFile) member;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a description of this reference, suitable to be shown to the user
+ *
+ * @return a display name for the reference
+ */
+ public String getDisplayName() {
+ // The ID is deliberately kept in a pretty user-readable format but we could
+ // consider prepending layout/ on ids that don't have it (to make the display
+ // more uniform) or ripping out all layout[-constraint] prefixes out and
+ // instead prepending @ etc.
+ return mId;
+ }
+
+ /**
+ * Returns the name of the reference, suitable for resource lookup. For example,
+ * for "res/layout/main.xml", as well as for "res/layout-land/main.xml", this
+ * would be "main".
+ *
+ * @return the resource name of the reference
+ */
+ public String getName() {
+ if (mName == null) {
+ mName = mId;
+ int index = mName.lastIndexOf(WS_SEP);
+ if (index != -1) {
+ mName = mName.substring(index + 1);
+ }
+ }
+
+ return mName;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mId == null) ? 0 : mId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Reference other = (Reference) obj;
+ if (mId == null) {
+ if (other.mId != null)
+ return false;
+ } else if (!mId.equals(other.mId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Reference [getId()=" + getId() //$NON-NLS-1$
+ + ", getDisplayName()=" + getDisplayName() //$NON-NLS-1$
+ + ", getName()=" + getName() //$NON-NLS-1$
+ + ", getFile()=" + getFile() + "]"; //$NON-NLS-1$
+ }
+
+ /**
+ * Creates a reference to the given file
+ *
+ * @param file the file to create a reference for
+ * @return a reference to the given file
+ */
+ public static Reference create(IFile file) {
+ return new Reference(file.getProject(), getMapKey(file));
+ }
+
+ /**
+ * Returns the resource name of this layout, such as {@code @layout/foo}.
+ *
+ * @return the resource name
+ */
+ public String getResourceName() {
+ return '@' + FD_RES_LAYOUT + '/' + getName();
+ }
+ }
+
+ /**
+ * Returns a collection of layouts (expressed as resource names, such as
+ * {@code @layout/foo} which would be invalid includes in the given layout
+ * (because it would introduce a cycle)
+ *
+ * @param layout the layout file to check for cyclic dependencies from
+ * @return a collection of layout resources which cannot be included from
+ * the given layout, never null
+ */
+ public Collection<String> getInvalidIncludes(IFile layout) {
+ IProject project = layout.getProject();
+ Reference self = Reference.create(layout);
+
+ // Add anyone who transitively can reach this file via includes.
+ LinkedList<Reference> queue = new LinkedList<Reference>();
+ List<Reference> invalid = new ArrayList<Reference>();
+ queue.add(self);
+ invalid.add(self);
+ Set<String> seen = new HashSet<String>();
+ seen.add(self.getId());
+ while (!queue.isEmpty()) {
+ Reference reference = queue.removeFirst();
+ String refId = reference.getId();
+
+ // Look up both configuration specific includes as well as includes in the
+ // base versions
+ List<String> included = getIncludedBy(refId);
+ if (refId.indexOf('/') != -1) {
+ List<String> baseIncluded = getIncludedBy(reference.getName());
+ if (included == null) {
+ included = baseIncluded;
+ } else if (baseIncluded != null) {
+ included = new ArrayList<String>(included);
+ included.addAll(baseIncluded);
+ }
+ }
+
+ if (included != null && included.size() > 0) {
+ for (String id : included) {
+ if (!seen.contains(id)) {
+ seen.add(id);
+ Reference ref = new Reference(project, id);
+ invalid.add(ref);
+ queue.addLast(ref);
+ }
+ }
+ }
+ }
+
+ List<String> result = new ArrayList<String>();
+ for (Reference reference : invalid) {
+ result.add(reference.getResourceName());
+ }
+
+ return result;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
new file mode 100644
index 000000000..81c03edd5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.VisibleForTesting;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The {@link IncludeOverlay} class renders masks to -partially- hide everything outside
+ * an included file's own content. This overlay is in use when you are editing an included
+ * file shown within a different file's context (e.g. "Show In > other").
+ */
+public class IncludeOverlay extends Overlay {
+ /** Mask transparency - 0 is transparent, 255 is opaque */
+ private static final int MASK_TRANSPARENCY = 160;
+
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs an {@link IncludeOverlay} tied to the given canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+ */
+ public IncludeOverlay(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ List<Rectangle> includedBounds = viewHierarchy.getIncludedBounds();
+ if (includedBounds == null || includedBounds.size() == 0) {
+ // We don't support multiple included children yet. When that works,
+ // this code should use a BSP tree to figure out which regions to paint
+ // to leave holes in the mask.
+ return;
+ }
+
+ Image image = mCanvas.getImageOverlay().getImage();
+ if (image == null) {
+ return;
+ }
+
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(MASK_TRANSPARENCY);
+ Color bg = gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+ gc.setBackground(bg);
+
+ CanvasViewInfo root = viewHierarchy.getRoot();
+ Rectangle whole = root.getAbsRect();
+ whole = new Rectangle(whole.x, whole.y, whole.width + 1, whole.height + 1);
+ Collection<Rectangle> masks = subtractRectangles(whole, includedBounds);
+
+ for (Rectangle mask : masks) {
+ ControlPoint topLeft = LayoutPoint.create(mCanvas, mask.x, mask.y).toControl();
+ ControlPoint bottomRight = LayoutPoint.create(mCanvas, mask.x + mask.width,
+ mask.y + mask.height).toControl();
+ int x1 = topLeft.x;
+ int y1 = topLeft.y;
+ int x2 = bottomRight.x;
+ int y2 = bottomRight.y;
+
+ gc.fillRectangle(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ gc.setAlpha(oldAlpha);
+ }
+
+ /**
+ * Given a Rectangle, remove holes from it (specified as a collection of Rectangles) such
+ * that the result is a list of rectangles that cover everything that is not a hole.
+ *
+ * @param rectangle the rectangle to subtract from
+ * @param holes the holes to subtract from the rectangle
+ * @return a list of sub rectangles that remain after subtracting out the given list of holes
+ */
+ @VisibleForTesting
+ static Collection<Rectangle> subtractRectangles(
+ Rectangle rectangle, Collection<Rectangle> holes) {
+ List<Rectangle> result = new ArrayList<Rectangle>();
+ result.add(rectangle);
+
+ for (Rectangle hole : holes) {
+ List<Rectangle> tempResult = new ArrayList<Rectangle>();
+ for (Rectangle r : result) {
+ if (hole.intersects(r)) {
+ // Clip the hole to fit the rectangle bounds
+ Rectangle h = hole.intersection(r);
+
+ // Split the rectangle
+
+ // Above (includes the NW and NE corners)
+ if (h.y > r.y) {
+ tempResult.add(new Rectangle(r.x, r.y, r.width, h.y - r.y));
+ }
+
+ // Left (not including corners)
+ if (h.x > r.x) {
+ tempResult.add(new Rectangle(r.x, h.y, h.x - r.x, h.height));
+ }
+
+ int hx2 = h.x + h.width;
+ int hy2 = h.y + h.height;
+ int rx2 = r.x + r.width;
+ int ry2 = r.y + r.height;
+
+ // Below (includes the SW and SE corners)
+ if (hy2 < ry2) {
+ tempResult.add(new Rectangle(r.x, hy2, r.width, ry2 - hy2));
+ }
+
+ // Right (not including corners)
+ if (hx2 < rx2) {
+ tempResult.add(new Rectangle(hx2, h.y, rx2 - hx2, h.height));
+ }
+ } else {
+ tempResult.add(r);
+ }
+ }
+
+ result = tempResult;
+ }
+
+ return result;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java
new file mode 100644
index 000000000..1b1bd23c4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
+import com.android.ide.common.api.RuleAction.Separator;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Screen;
+import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog;
+import com.google.common.base.Strings;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Toolbar shown at the top of the layout editor, which adds a number of context-sensitive
+ * layout actions (as well as zooming controls on the right).
+ */
+public class LayoutActionBar extends Composite {
+ private GraphicalEditorPart mEditor;
+ private ToolBar mLayoutToolBar;
+ private ToolBar mLintToolBar;
+ private ToolBar mZoomToolBar;
+ private ToolItem mZoomRealSizeButton;
+ private ToolItem mZoomOutButton;
+ private ToolItem mZoomResetButton;
+ private ToolItem mZoomInButton;
+ private ToolItem mZoomFitButton;
+ private ToolItem mLintButton;
+ private List<RuleAction> mPrevActions;
+
+ /**
+ * Creates a new {@link LayoutActionBar} and adds it to the given parent.
+ *
+ * @param parent the parent composite to add the actions bar to
+ * @param style the SWT style to apply
+ * @param editor the associated layout editor
+ */
+ public LayoutActionBar(Composite parent, int style, GraphicalEditorPart editor) {
+ super(parent, style | SWT.NO_FOCUS);
+ mEditor = editor;
+
+ GridLayout layout = new GridLayout(3, false);
+ setLayout(layout);
+
+ mLayoutToolBar = new ToolBar(this, /*SWT.WRAP |*/ SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+ mLayoutToolBar.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
+ mZoomToolBar = createZoomControls();
+ mZoomToolBar.setLayoutData(new GridData(SWT.END, SWT.BEGINNING, false, false));
+ mLintToolBar = createLintControls();
+
+ GridData lintData = new GridData(SWT.END, SWT.BEGINNING, false, false);
+ lintData.exclude = true;
+ mLintToolBar.setLayoutData(lintData);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ mPrevActions = null;
+ }
+
+ /** Updates the layout contents based on the current selection */
+ void updateSelection() {
+ NodeProxy parent = null;
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ if (selections.size() > 0) {
+ // TODO: better handle multi-selection -- maybe we should disable it or
+ // something.
+ // What if you select children with different parents? Of different types?
+ // etc.
+ NodeProxy node = selections.get(0).getNode();
+ if (node != null && node.getParent() != null) {
+ parent = (NodeProxy) node.getParent();
+ }
+ }
+
+ if (parent == null) {
+ // Show the background's properties
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root == null) {
+ return;
+ }
+ parent = canvas.getNodeFactory().create(root);
+ selections = Collections.emptyList();
+ }
+
+ RulesEngine engine = mEditor.getRulesEngine();
+ List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>();
+ for (SelectionItem item : selections) {
+ selectedNodes.add(item.getNode());
+ }
+ List<RuleAction> actions = new ArrayList<RuleAction>();
+ engine.callAddLayoutActions(actions, parent, selectedNodes);
+
+ // Place actions in the correct order (the actions may come from different
+ // rules and should be merged properly via sorting keys)
+ Collections.sort(actions);
+
+ // Add in actions for the child as well, if there is exactly one.
+ // These are not merged into the parent list of actions; they are appended
+ // at the end.
+ int index = -1;
+ String label = null;
+ if (selectedNodes.size() == 1) {
+ List<RuleAction> itemActions = new ArrayList<RuleAction>();
+ NodeProxy selectedNode = selectedNodes.get(0);
+ engine.callAddLayoutActions(itemActions, selectedNode, null);
+ if (itemActions.size() > 0) {
+ Collections.sort(itemActions);
+
+ if (!(itemActions.get(0) instanceof RuleAction.Separator)) {
+ actions.add(RuleAction.createSeparator(0));
+ }
+ label = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (label != null) {
+ label = BaseViewRule.stripIdPrefix(label);
+ index = actions.size();
+ }
+ actions.addAll(itemActions);
+ }
+ }
+
+ if (!updateActions(actions)) {
+ updateToolbar(actions, index, label);
+ }
+ mPrevActions = actions;
+ }
+
+ /** Update the toolbar widgets */
+ private void updateToolbar(final List<RuleAction> actions, final int labelIndex,
+ final String label) {
+ if (mLayoutToolBar == null || mLayoutToolBar.isDisposed()) {
+ return;
+ }
+ for (ToolItem c : mLayoutToolBar.getItems()) {
+ c.dispose();
+ }
+ mLayoutToolBar.pack();
+ addActions(actions, labelIndex, label);
+ mLayoutToolBar.pack();
+ mLayoutToolBar.layout();
+ }
+
+ /**
+ * Attempts to update the existing toolbar actions, if the action list is
+ * similar to the current list. Returns false if this cannot be done and the
+ * contents must be replaced.
+ */
+ private boolean updateActions(@NonNull List<RuleAction> actions) {
+ List<RuleAction> before = mPrevActions;
+ List<RuleAction> after = actions;
+
+ if (before == null) {
+ return false;
+ }
+
+ if (!before.equals(after) || after.size() > mLayoutToolBar.getItemCount()) {
+ return false;
+ }
+
+ int actionIndex = 0;
+ for (int i = 0, max = mLayoutToolBar.getItemCount(); i < max; i++) {
+ ToolItem item = mLayoutToolBar.getItem(i);
+ int style = item.getStyle();
+ Object data = item.getData();
+ if (data != null) {
+ // One action can result in multiple toolbar items (e.g. a choice action
+ // can result in multiple radio buttons), so we've have to replace all of
+ // them with the corresponding new action
+ RuleAction prevAction = before.get(actionIndex);
+ while (prevAction != data) {
+ actionIndex++;
+ if (actionIndex == before.size()) {
+ return false;
+ }
+ prevAction = before.get(actionIndex);
+ if (prevAction == data) {
+ break;
+ } else if (!(prevAction instanceof RuleAction.Separator)) {
+ return false;
+ }
+ }
+ RuleAction newAction = after.get(actionIndex);
+ assert newAction.equals(prevAction); // Maybe I can do this lazily instead?
+
+ // Update action binding to the new action
+ item.setData(newAction);
+
+ // Sync button states: the checked state is not considered part of
+ // RuleAction equality
+ if ((style & SWT.CHECK) != 0) {
+ assert newAction instanceof Toggle;
+ Toggle toggle = (Toggle) newAction;
+ item.setSelection(toggle.isChecked());
+ } else if ((style & SWT.RADIO) != 0) {
+ assert newAction instanceof Choices;
+ Choices choices = (Choices) newAction;
+ String current = choices.getCurrent();
+ String id = (String) item.getData(ATTR_ID);
+ boolean selected = Strings.nullToEmpty(current).equals(id);
+ item.setSelection(selected);
+ }
+ } else {
+ // Must be a separator, or a label (which we insert for nested widgets)
+ assert (style & SWT.SEPARATOR) != 0 || !item.getText().isEmpty() : item;
+ }
+ }
+
+ return true;
+ }
+
+ private void addActions(List<RuleAction> actions, int labelIndex, String label) {
+ if (actions.size() > 0) {
+ // Flag used to indicate that if there are any actions -after- this, it
+ // should be separated from this current action (we don't unconditionally
+ // add a separator at the end of these groups in case there are no more
+ // actions at the end so that we don't have a trailing separator)
+ boolean needSeparator = false;
+
+ int index = 0;
+ for (RuleAction action : actions) {
+ if (index == labelIndex) {
+ final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH);
+ button.setText(label);
+ needSeparator = false;
+ }
+ index++;
+
+ if (action instanceof Separator) {
+ addSeparator(mLayoutToolBar);
+ needSeparator = false;
+ continue;
+ } else if (needSeparator) {
+ addSeparator(mLayoutToolBar);
+ needSeparator = false;
+ }
+
+ if (action instanceof RuleAction.Choices) {
+ RuleAction.Choices choices = (Choices) action;
+ if (!choices.isRadio()) {
+ addDropdown(choices);
+ } else {
+ addSeparator(mLayoutToolBar);
+ addRadio(choices);
+ needSeparator = true;
+ }
+ } else if (action instanceof RuleAction.Toggle) {
+ addToggle((Toggle) action);
+ } else {
+ addPlainAction(action);
+ }
+ }
+ }
+ }
+
+ /** Add a separator to the toolbar, unless there already is one there at the end already */
+ private static void addSeparator(ToolBar toolBar) {
+ int n = toolBar.getItemCount();
+ if (n > 0 && (toolBar.getItem(n - 1).getStyle() & SWT.SEPARATOR) == 0) {
+ ToolItem separator = new ToolItem(toolBar, SWT.SEPARATOR);
+ separator.setWidth(15);
+ }
+ }
+
+ private void addToggle(Toggle toggle) {
+ final ToolItem button = new ToolItem(mLayoutToolBar, SWT.CHECK);
+
+ URL iconUrl = toggle.getIconUrl();
+ String title = toggle.getTitle();
+ if (iconUrl != null) {
+ button.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ button.setToolTipText(title);
+ } else {
+ button.setText(title);
+ }
+ button.setData(toggle);
+
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Toggle toggle = (Toggle) button.getData();
+ toggle.getCallback().action(toggle, getSelectedNodes(),
+ toggle.getId(), button.getSelection());
+ updateSelection();
+ }
+ });
+ if (toggle.isChecked()) {
+ button.setSelection(true);
+ }
+ }
+
+ private List<INode> getSelectedNodes() {
+ List<SelectionItem> selections =
+ mEditor.getCanvasControl().getSelectionManager().getSelections();
+ List<INode> nodes = new ArrayList<INode>(selections.size());
+ for (SelectionItem item : selections) {
+ nodes.add(item.getNode());
+ }
+
+ return nodes;
+ }
+
+
+ private void addPlainAction(RuleAction menuAction) {
+ final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH);
+
+ URL iconUrl = menuAction.getIconUrl();
+ String title = menuAction.getTitle();
+ if (iconUrl != null) {
+ button.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ button.setToolTipText(title);
+ } else {
+ button.setText(title);
+ }
+ button.setData(menuAction);
+
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ RuleAction menuAction = (RuleAction) button.getData();
+ menuAction.getCallback().action(menuAction, getSelectedNodes(), menuAction.getId(),
+ false);
+ updateSelection();
+ }
+ });
+ }
+
+ private void addRadio(RuleAction.Choices choices) {
+ List<URL> icons = choices.getIconUrls();
+ List<String> titles = choices.getTitles();
+ List<String> ids = choices.getIds();
+ String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$
+
+ assert icons != null;
+ assert icons.size() == titles.size();
+
+ for (int i = 0; i < icons.size(); i++) {
+ URL iconUrl = icons.get(i);
+ String title = titles.get(i);
+ final String id = ids.get(i);
+ final ToolItem item = new ToolItem(mLayoutToolBar, SWT.RADIO);
+ item.setToolTipText(title);
+ item.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ item.setData(choices);
+ item.setData(ATTR_ID, id);
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (item.getSelection()) {
+ RuleAction.Choices choices = (Choices) item.getData();
+ choices.getCallback().action(choices, getSelectedNodes(), id, null);
+ updateSelection();
+ }
+ }
+ });
+ boolean selected = current.equals(id);
+ if (selected) {
+ item.setSelection(true);
+ }
+ }
+ }
+
+ private void addDropdown(RuleAction.Choices choices) {
+ final ToolItem combo = new ToolItem(mLayoutToolBar, SWT.DROP_DOWN);
+ URL iconUrl = choices.getIconUrl();
+ if (iconUrl != null) {
+ combo.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ combo.setToolTipText(choices.getTitle());
+ } else {
+ combo.setText(choices.getTitle());
+ }
+ combo.setData(choices);
+
+ Listener menuListener = new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ Menu menu = new Menu(mLayoutToolBar.getShell(), SWT.POP_UP);
+ RuleAction.Choices choices = (Choices) combo.getData();
+ List<URL> icons = choices.getIconUrls();
+ List<String> titles = choices.getTitles();
+ List<String> ids = choices.getIds();
+ String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$
+
+ for (int i = 0; i < titles.size(); i++) {
+ String title = titles.get(i);
+ final String id = ids.get(i);
+ URL itemIconUrl = icons != null && icons.size() > 0 ? icons.get(i) : null;
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+ if (itemIconUrl != null) {
+ Image itemIcon = IconFactory.getInstance().getIcon(itemIconUrl);
+ item.setImage(itemIcon);
+ }
+
+ boolean selected = id.equals(current);
+ if (selected) {
+ item.setSelection(true);
+ }
+
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ RuleAction.Choices choices = (Choices) combo.getData();
+ choices.getCallback().action(choices, getSelectedNodes(), id, null);
+ updateSelection();
+ }
+ });
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+ };
+ combo.addListener(SWT.Selection, menuListener);
+ }
+
+ // ---- Zoom Controls ----
+
+ @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused
+ private ToolBar createZoomControls() {
+ ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+
+ IconFactory iconFactory = IconFactory.getInstance();
+ mZoomRealSizeButton = new ToolItem(toolBar, SWT.CHECK);
+ mZoomRealSizeButton.setToolTipText("Emulate Real Size");
+ mZoomRealSizeButton.setImage(iconFactory.getIcon("zoomreal")); //$NON-NLS-1$);
+ mZoomRealSizeButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ boolean newState = mZoomRealSizeButton.getSelection();
+ if (rescaleToReal(newState)) {
+ mZoomOutButton.setEnabled(!newState);
+ mZoomResetButton.setEnabled(!newState);
+ mZoomInButton.setEnabled(!newState);
+ mZoomFitButton.setEnabled(!newState);
+ } else {
+ mZoomRealSizeButton.setSelection(!newState);
+ }
+ }
+ });
+
+ mZoomFitButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomFitButton.setToolTipText("Zoom to Fit (0)");
+ mZoomFitButton.setImage(iconFactory.getIcon("zoomfit")); //$NON-NLS-1$);
+ mZoomFitButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ rescaleToFit(true);
+ }
+ });
+
+ mZoomResetButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomResetButton.setToolTipText("Reset Zoom to 100% (1)");
+ mZoomResetButton.setImage(iconFactory.getIcon("zoom100")); //$NON-NLS-1$);
+ mZoomResetButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ resetScale();
+ }
+ });
+
+ // Group zoom in/out separately
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mZoomOutButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomOutButton.setToolTipText("Zoom Out (-)");
+ mZoomOutButton.setImage(iconFactory.getIcon("zoomminus")); //$NON-NLS-1$);
+ mZoomOutButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ rescale(-1);
+ }
+ });
+
+ mZoomInButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomInButton.setToolTipText("Zoom In (+)");
+ mZoomInButton.setImage(iconFactory.getIcon("zoomplus")); //$NON-NLS-1$);
+ mZoomInButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ rescale(+1);
+ }
+ });
+
+ return toolBar;
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused
+ private ToolBar createLintControls() {
+ ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+
+ // Separate from adjacent toolbar
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ mLintButton = new ToolItem(toolBar, SWT.PUSH);
+ mLintButton.setToolTipText("Show Lint Warnings for this Layout");
+ mLintButton.setImage(sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK));
+ mLintButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ CommonXmlEditor editor = mEditor.getEditorDelegate().getEditor();
+ IFile file = editor.getInputFile();
+ if (file != null) {
+ EclipseLintClient.showErrors(getShell(), file, editor);
+ }
+ }
+ });
+
+ return toolBar;
+ }
+
+ /**
+ * Updates the lint indicator state in the given layout editor
+ */
+ public void updateErrorIndicator() {
+ updateErrorIndicator(mEditor.getEditedFile());
+ }
+
+ /**
+ * Updates the lint indicator state for the given file
+ *
+ * @param file the file to show the indicator status for
+ */
+ public void updateErrorIndicator(IFile file) {
+ IMarker[] markers = EclipseLintClient.getMarkers(file);
+ updateErrorIndicator(markers.length);
+ }
+
+ /**
+ * Sets whether the action bar should show the "lint warnings" button
+ *
+ * @param hasLintWarnings whether there are lint errors to be shown
+ */
+ private void updateErrorIndicator(final int markerCount) {
+ Display display = getDisplay();
+ if (display.getThread() != Thread.currentThread()) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!isDisposed()) {
+ updateErrorIndicator(markerCount);
+ }
+ }
+ });
+ return;
+ }
+
+ GridData layoutData = (GridData) mLintToolBar.getLayoutData();
+ Integer existing = (Integer) mLintToolBar.getData();
+ Integer current = Integer.valueOf(markerCount);
+ if (!current.equals(existing)) {
+ mLintToolBar.setData(current);
+ boolean layout = false;
+ boolean hasLintWarnings = markerCount > 0 && AdtPrefs.getPrefs().isLintOnSave();
+ if (layoutData.exclude == hasLintWarnings) {
+ layoutData.exclude = !hasLintWarnings;
+ mLintToolBar.setVisible(hasLintWarnings);
+ layout = true;
+ }
+ if (markerCount > 0) {
+ String iconName = "";
+ switch (markerCount) {
+ case 1: iconName = "lint1"; break; //$NON-NLS-1$
+ case 2: iconName = "lint2"; break; //$NON-NLS-1$
+ case 3: iconName = "lint3"; break; //$NON-NLS-1$
+ case 4: iconName = "lint4"; break; //$NON-NLS-1$
+ case 5: iconName = "lint5"; break; //$NON-NLS-1$
+ case 6: iconName = "lint6"; break; //$NON-NLS-1$
+ case 7: iconName = "lint7"; break; //$NON-NLS-1$
+ case 8: iconName = "lint8"; break; //$NON-NLS-1$
+ case 9: iconName = "lint9"; break; //$NON-NLS-1$
+ default: iconName = "lint9p"; break;//$NON-NLS-1$
+ }
+ mLintButton.setImage(IconFactory.getInstance().getIcon(iconName));
+ }
+ if (layout) {
+ layout();
+ }
+ redraw();
+ }
+ }
+
+ /**
+ * Returns true if zooming in/out/to-fit/etc is allowed (which is not the case while
+ * emulating real size)
+ *
+ * @return true if zooming is allowed
+ */
+ boolean isZoomingAllowed() {
+ return mZoomInButton.isEnabled();
+ }
+
+ boolean isZoomingRealSize() {
+ return mZoomRealSizeButton.getSelection();
+ }
+
+ /**
+ * Rescales canvas.
+ * @param direction +1 for zoom in, -1 for zoom out
+ */
+ void rescale(int direction) {
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ double s = canvas.getScale();
+
+ if (direction > 0) {
+ s = s * 1.2;
+ } else {
+ s = s / 1.2;
+ }
+
+ // Some operations are faster if the zoom is EXACTLY 1.0 rather than ALMOST 1.0.
+ // (This is because there is a fast-path when image copying and the scale is 1.0;
+ // in that case it does not have to do any scaling).
+ //
+ // If you zoom out 10 times and then back in 10 times, small rounding errors mean
+ // that you end up with a scale=1.0000000000000004. In the cases, when you get close
+ // to 1.0, just make the zoom an exact 1.0.
+ if (Math.abs(s-1.0) < 0.0001) {
+ s = 1.0;
+ }
+
+ canvas.setScale(s, true /*redraw*/);
+ }
+
+ /**
+ * Reset the canvas scale to 100%
+ */
+ void resetScale() {
+ mEditor.getCanvasControl().setScale(1, true /*redraw*/);
+ }
+
+ /**
+ * Reset the canvas scale to best fit (so content is as large as possible without scrollbars)
+ */
+ void rescaleToFit(boolean onlyZoomOut) {
+ mEditor.getCanvasControl().setFitScale(onlyZoomOut, true /*allowZoomIn*/);
+ }
+
+ boolean rescaleToReal(boolean real) {
+ if (real) {
+ return computeAndSetRealScale(true /*redraw*/);
+ } else {
+ // reset the scale to 100%
+ mEditor.getCanvasControl().setScale(1, true /*redraw*/);
+ return true;
+ }
+ }
+
+ boolean computeAndSetRealScale(boolean redraw) {
+ // compute average dpi of X and Y
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ Configuration config = chooser.getConfiguration();
+ Device device = config.getDevice();
+ Screen screen = device.getDefaultHardware().getScreen();
+ double dpi = (screen.getXdpi() + screen.getYdpi()) / 2.;
+
+ // get the monitor dpi
+ float monitor = AdtPrefs.getPrefs().getMonitorDensity();
+ if (monitor == 0.f) {
+ ResolutionChooserDialog dialog = new ResolutionChooserDialog(chooser.getShell());
+ if (dialog.open() == Window.OK) {
+ monitor = dialog.getDensity();
+ AdtPrefs.getPrefs().setMonitorDensity(monitor);
+ } else {
+ return false;
+ }
+ }
+
+ mEditor.getCanvasControl().setScale(monitor / dpi, redraw);
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
new file mode 100644
index 000000000..814b82cec
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
@@ -0,0 +1,1720 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.lint.LintEditAction;
+import com.android.resources.Density;
+
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
+import org.eclipse.ui.actions.ContributionItemFactory;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.w3c.dom.Node;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Displays the image rendered by the {@link GraphicalEditorPart} and handles
+ * the interaction with the widgets.
+ * <p/>
+ * {@link LayoutCanvas} implements the "Canvas" control. The editor part
+ * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper
+ * around this control.
+ * <p/>
+ * The LayoutCanvas contains the painting logic for the canvas. Selection,
+ * clipboard, view management etc. is handled in separate helper classes.
+ *
+ * @since GLE2
+ */
+@SuppressWarnings("restriction") // For WorkBench "Show In" support
+public class LayoutCanvas extends Canvas {
+ private final static QualifiedName NAME_ZOOM =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "zoom");//$NON-NLS-1$
+
+ private static final boolean DEBUG = false;
+
+ static final String PREFIX_CANVAS_ACTION = "canvas_action_"; //$NON-NLS-1$
+
+ /** The layout editor that uses this layout canvas. */
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ /** The Rules Engine, associated with the current project. */
+ private RulesEngine mRulesEngine;
+
+ /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the
+ * context of {@link #onPaint(PaintEvent)}; otherwise it is null. */
+ private GCWrapper mGCWrapper;
+
+ /** Default font used on the canvas. Do not dispose, it's a system font. */
+ private Font mFont;
+
+ /** Current hover view info. Null when no mouse hover. */
+ private CanvasViewInfo mHoverViewInfo;
+
+ /** When true, always display the outline of all views. */
+ private boolean mShowOutline;
+
+ /** When true, display the outline of all empty parent views. */
+ private boolean mShowInvisible;
+
+ /** Drop target associated with this composite. */
+ private DropTarget mDropTarget;
+
+ /** Factory that can create {@link INode} proxies. */
+ private final @NonNull NodeFactory mNodeFactory = new NodeFactory(this);
+
+ /** Vertical scaling & scrollbar information. */
+ private final CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private final CanvasTransform mHScale;
+
+ /** Drag source associated with this canvas. */
+ private DragSource mDragSource;
+
+ /**
+ * The current Outline Page, to set its model.
+ * It isn't possible to call OutlinePage2.dispose() in this.dispose().
+ * this.dispose() is called from GraphicalEditorPart.dispose(),
+ * when page's widget is already disposed.
+ * Added the DisposeListener to OutlinePage2 in order to correctly dispose this page.
+ **/
+ private OutlinePage mOutlinePage;
+
+ /** Delete action for the Edit or context menu. */
+ private Action mDeleteAction;
+
+ /** Select-All action for the Edit or context menu. */
+ private Action mSelectAllAction;
+
+ /** Paste action for the Edit or context menu. */
+ private Action mPasteAction;
+
+ /** Cut action for the Edit or context menu. */
+ private Action mCutAction;
+
+ /** Copy action for the Edit or context menu. */
+ private Action mCopyAction;
+
+ /** Undo action: delegates to the text editor */
+ private IAction mUndoAction;
+
+ /** Redo action: delegates to the text editor */
+ private IAction mRedoAction;
+
+ /** Root of the context menu. */
+ private MenuManager mMenuManager;
+
+ /** The view hierarchy associated with this canvas. */
+ private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this);
+
+ /** The selection in the canvas. */
+ private final SelectionManager mSelectionManager = new SelectionManager(this);
+
+ /** The overlay which paints the optional outline. */
+ private OutlineOverlay mOutlineOverlay;
+
+ /** The overlay which paints outlines around empty children */
+ private EmptyViewsOverlay mEmptyOverlay;
+
+ /** The overlay which paints the mouse hover. */
+ private HoverOverlay mHoverOverlay;
+
+ /** The overlay which paints the lint warnings */
+ private LintOverlay mLintOverlay;
+
+ /** The overlay which paints the selection. */
+ private SelectionOverlay mSelectionOverlay;
+
+ /** The overlay which paints the rendered layout image. */
+ private ImageOverlay mImageOverlay;
+
+ /** The overlay which paints masks hiding everything but included content. */
+ private IncludeOverlay mIncludeOverlay;
+
+ /** Configuration previews shown next to the layout */
+ private final RenderPreviewManager mPreviewManager;
+
+ /**
+ * Gesture Manager responsible for identifying mouse, keyboard and drag and
+ * drop events.
+ */
+ private final GestureManager mGestureManager = new GestureManager(this);
+
+ /**
+ * When set, performs a zoom-to-fit when the next rendering image arrives.
+ */
+ private boolean mZoomFitNextImage;
+
+ /**
+ * Native clipboard support.
+ */
+ private ClipboardSupport mClipboardSupport;
+
+ /** Tooltip manager for lint warnings */
+ private LintTooltipManager mLintTooltipManager;
+
+ private Color mBackgroundColor;
+
+ /**
+ * Creates a new {@link LayoutCanvas} widget
+ *
+ * @param editorDelegate the associated editor delegate
+ * @param rulesEngine the rules engine
+ * @param parent parent SWT widget
+ * @param style the SWT style
+ */
+ public LayoutCanvas(LayoutEditorDelegate editorDelegate,
+ RulesEngine rulesEngine,
+ Composite parent,
+ int style) {
+ super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL);
+ mEditorDelegate = editorDelegate;
+ mRulesEngine = rulesEngine;
+
+ mBackgroundColor = new Color(parent.getDisplay(), 150, 150, 150);
+ setBackground(mBackgroundColor);
+
+ mClipboardSupport = new ClipboardSupport(this, parent);
+ mHScale = new CanvasTransform(this, getHorizontalBar());
+ mVScale = new CanvasTransform(this, getVerticalBar());
+ mPreviewManager = new RenderPreviewManager(this);
+
+ // Unit test suite passes a null here; TODO: Replace with mocking
+ IFile file = editorDelegate != null ? editorDelegate.getEditor().getInputFile() : null;
+ if (file != null) {
+ String zoom = AdtPlugin.getFileProperty(file, NAME_ZOOM);
+ if (zoom != null) {
+ try {
+ double initialScale = Double.parseDouble(zoom);
+ if (initialScale > 0.1) {
+ mHScale.setScale(initialScale);
+ mVScale.setScale(initialScale);
+ }
+ } catch (NumberFormatException nfe) {
+ // Ignore - use zoom=100%
+ }
+ } else {
+ mZoomFitNextImage = true;
+ }
+ }
+
+ mGCWrapper = new GCWrapper(mHScale, mVScale);
+
+ Display display = getDisplay();
+ mFont = display.getSystemFont();
+
+ // --- Set up graphic overlays
+ // mOutlineOverlay and mEmptyOverlay are initialized lazily
+ mHoverOverlay = new HoverOverlay(this, mHScale, mVScale);
+ mHoverOverlay.create(display);
+ mSelectionOverlay = new SelectionOverlay(this);
+ mSelectionOverlay.create(display);
+ mImageOverlay = new ImageOverlay(this, mHScale, mVScale);
+ mIncludeOverlay = new IncludeOverlay(this);
+ mImageOverlay.create(display);
+ mLintOverlay = new LintOverlay(this);
+ mLintOverlay.create(display);
+
+ // --- Set up listeners
+ addPaintListener(new PaintListener() {
+ @Override
+ public void paintControl(PaintEvent e) {
+ onPaint(e);
+ }
+ });
+
+ addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ super.controlResized(e);
+
+ // Check editor state:
+ LayoutWindowCoordinator coordinator = null;
+ IEditorSite editorSite = getEditorDelegate().getEditor().getEditorSite();
+ IWorkbenchWindow window = editorSite.getWorkbenchWindow();
+ if (window != null) {
+ coordinator = LayoutWindowCoordinator.get(window, false);
+ if (coordinator != null) {
+ coordinator.syncMaximizedState(editorSite.getPage());
+ }
+ }
+
+ updateScrollBars();
+
+ // Update the zoom level in the canvas when you toggle the zoom
+ if (coordinator != null) {
+ mZoomCheck.run();
+ } else {
+ // During startup, delay updates which can trigger further layout
+ getDisplay().asyncExec(mZoomCheck);
+
+ }
+ }
+ });
+
+ // --- setup drag'n'drop ---
+ // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
+
+ mDropTarget = createDropTarget(this);
+ mDragSource = createDragSource(this);
+ mGestureManager.registerListeners(mDragSource, mDropTarget);
+
+ if (mEditorDelegate == null) {
+ // TODO: In another CL we should use EasyMock/objgen to provide an editor.
+ return; // Unit test
+ }
+
+ // --- setup context menu ---
+ setupGlobalActionHandlers();
+ createContextMenu();
+
+ // --- setup outline ---
+ // Get the outline associated with this editor, if any and of the right type.
+ if (editorDelegate != null) {
+ mOutlinePage = editorDelegate.getGraphicalOutline();
+ }
+
+ mLintTooltipManager = new LintTooltipManager(this);
+ mLintTooltipManager.register();
+ }
+
+ void updateScrollBars() {
+ Rectangle clientArea = getClientArea();
+ Image image = mImageOverlay.getImage();
+ if (image != null) {
+ ImageData imageData = image.getImageData();
+ int clientWidth = clientArea.width;
+ int clientHeight = clientArea.height;
+
+ int imageWidth = imageData.width;
+ int imageHeight = imageData.height;
+
+ int fullWidth = imageWidth;
+ int fullHeight = imageHeight;
+
+ if (mPreviewManager.hasPreviews()) {
+ fullHeight = Math.max(fullHeight,
+ (int) (mPreviewManager.getHeight() / mHScale.getScale()));
+ }
+
+ if (clientWidth == 0) {
+ clientWidth = imageWidth;
+ Shell shell = getShell();
+ if (shell != null) {
+ org.eclipse.swt.graphics.Point size = shell.getSize();
+ if (size.x > 0) {
+ clientWidth = size.x * 70 / 100;
+ }
+ }
+ }
+ if (clientHeight == 0) {
+ clientHeight = imageHeight;
+ Shell shell = getShell();
+ if (shell != null) {
+ org.eclipse.swt.graphics.Point size = shell.getSize();
+ if (size.y > 0) {
+ clientWidth = size.y * 80 / 100;
+ }
+ }
+ }
+
+ mHScale.setSize(imageWidth, fullWidth, clientWidth);
+ mVScale.setSize(imageHeight, fullHeight, clientHeight);
+ }
+ }
+
+ private Runnable mZoomCheck = new Runnable() {
+ private Boolean mWasZoomed;
+
+ @Override
+ public void run() {
+ if (isDisposed()) {
+ return;
+ }
+
+ IEditorSite editorSite = getEditorDelegate().getEditor().getEditorSite();
+ IWorkbenchWindow window = editorSite.getWorkbenchWindow();
+ if (window != null) {
+ LayoutWindowCoordinator coordinator = LayoutWindowCoordinator.get(window, false);
+ if (coordinator != null) {
+ Boolean zoomed = coordinator.isEditorMaximized();
+ if (mWasZoomed != zoomed) {
+ if (mWasZoomed != null) {
+ LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar();
+ if (actionBar.isZoomingAllowed()) {
+ setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/);
+ }
+ }
+ mWasZoomed = zoomed;
+ }
+ }
+ }
+ }
+ };
+
+ void handleKeyPressed(KeyEvent e) {
+ // Set up backspace as an alias for the delete action within the canvas.
+ // On most Macs there is no delete key - though there IS a key labeled
+ // "Delete" and it sends a backspace key code! In short, for Macs we should
+ // treat backspace as delete, and it's harmless (and probably useful) to
+ // handle backspace for other platforms as well.
+ if (e.keyCode == SWT.BS) {
+ mDeleteAction.run();
+ } else if (e.keyCode == SWT.ESC) {
+ mSelectionManager.selectParent();
+ } else if (e.keyCode == DynamicContextMenu.DEFAULT_ACTION_KEY) {
+ mSelectionManager.performDefaultAction();
+ } else if (e.keyCode == 'r') {
+ // Keep key bindings in sync with {@link DynamicContextMenu#createPlainAction}
+ // TODO: Find a way to look up the Eclipse key bindings and attempt
+ // to use the current keymap's rename action.
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ // Command+Option+R
+ if ((e.stateMask & (SWT.MOD1 | SWT.MOD3)) == (SWT.MOD1 | SWT.MOD3)) {
+ mSelectionManager.performRename();
+ }
+ } else {
+ // Alt+Shift+R
+ if ((e.stateMask & (SWT.MOD2 | SWT.MOD3)) == (SWT.MOD2 | SWT.MOD3)) {
+ mSelectionManager.performRename();
+ }
+ }
+ } else {
+ // Zooming actions
+ char c = e.character;
+ LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar();
+ if (c == '1' && actionBar.isZoomingAllowed()) {
+ setScale(1, true);
+ } else if (c == '0' && actionBar.isZoomingAllowed()) {
+ setFitScale(true, true /*allowZoomIn*/);
+ } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0
+ && actionBar.isZoomingAllowed()) {
+ setFitScale(false, true /*allowZoomIn*/);
+ } else if ((c == '+' || c == '=') && actionBar.isZoomingAllowed()) {
+ if ((e.stateMask & SWT.MOD1) != 0) {
+ mPreviewManager.zoomIn();
+ } else {
+ actionBar.rescale(1);
+ }
+ } else if (c == '-' && actionBar.isZoomingAllowed()) {
+ if ((e.stateMask & SWT.MOD1) != 0) {
+ mPreviewManager.zoomOut();
+ } else {
+ actionBar.rescale(-1);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ mGestureManager.unregisterListeners(mDragSource, mDropTarget);
+
+ if (mLintTooltipManager != null) {
+ mLintTooltipManager.unregister();
+ mLintTooltipManager = null;
+ }
+
+ if (mDropTarget != null) {
+ mDropTarget.dispose();
+ mDropTarget = null;
+ }
+
+ if (mRulesEngine != null) {
+ mRulesEngine.dispose();
+ mRulesEngine = null;
+ }
+
+ if (mDragSource != null) {
+ mDragSource.dispose();
+ mDragSource = null;
+ }
+
+ if (mClipboardSupport != null) {
+ mClipboardSupport.dispose();
+ mClipboardSupport = null;
+ }
+
+ if (mGCWrapper != null) {
+ mGCWrapper.dispose();
+ mGCWrapper = null;
+ }
+
+ if (mOutlineOverlay != null) {
+ mOutlineOverlay.dispose();
+ mOutlineOverlay = null;
+ }
+
+ if (mEmptyOverlay != null) {
+ mEmptyOverlay.dispose();
+ mEmptyOverlay = null;
+ }
+
+ if (mHoverOverlay != null) {
+ mHoverOverlay.dispose();
+ mHoverOverlay = null;
+ }
+
+ if (mSelectionOverlay != null) {
+ mSelectionOverlay.dispose();
+ mSelectionOverlay = null;
+ }
+
+ if (mImageOverlay != null) {
+ mImageOverlay.dispose();
+ mImageOverlay = null;
+ }
+
+ if (mIncludeOverlay != null) {
+ mIncludeOverlay.dispose();
+ mIncludeOverlay = null;
+ }
+
+ if (mLintOverlay != null) {
+ mLintOverlay.dispose();
+ mLintOverlay = null;
+ }
+
+ if (mBackgroundColor != null) {
+ mBackgroundColor.dispose();
+ mBackgroundColor = null;
+ }
+
+ mPreviewManager.disposePreviews();
+ mViewHierarchy.dispose();
+ }
+
+ /**
+ * Returns the configuration preview manager for this canvas
+ *
+ * @return the configuration preview manager for this canvas
+ */
+ @NonNull
+ public RenderPreviewManager getPreviewManager() {
+ return mPreviewManager;
+ }
+
+ /** Returns the Rules Engine, associated with the current project. */
+ RulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+
+ /** Sets the Rules Engine, associated with the current project. */
+ void setRulesEngine(RulesEngine rulesEngine) {
+ mRulesEngine = rulesEngine;
+ }
+
+ /**
+ * Returns the factory to use to convert from {@link CanvasViewInfo} or from
+ * {@link UiViewElementNode} to {@link INode} proxies.
+ *
+ * @return the node factory
+ */
+ @NonNull
+ public NodeFactory getNodeFactory() {
+ return mNodeFactory;
+ }
+
+ /**
+ * Returns the GCWrapper used to paint view rules.
+ *
+ * @return The GCWrapper used to paint view rules
+ */
+ GCWrapper getGcWrapper() {
+ return mGCWrapper;
+ }
+
+ /**
+ * Returns the {@link LayoutEditorDelegate} associated with this canvas.
+ *
+ * @return the delegate
+ */
+ public LayoutEditorDelegate getEditorDelegate() {
+ return mEditorDelegate;
+ }
+
+ /**
+ * Returns the current {@link ImageOverlay} painting the rendered result
+ *
+ * @return the image overlay responsible for painting the rendered result, never null
+ */
+ ImageOverlay getImageOverlay() {
+ return mImageOverlay;
+ }
+
+ /**
+ * Returns the current {@link SelectionOverlay} painting the selection highlights
+ *
+ * @return the selection overlay responsible for painting the selection highlights,
+ * never null
+ */
+ SelectionOverlay getSelectionOverlay() {
+ return mSelectionOverlay;
+ }
+
+ /**
+ * Returns the {@link GestureManager} associated with this canvas.
+ *
+ * @return the {@link GestureManager} associated with this canvas, never null.
+ */
+ GestureManager getGestureManager() {
+ return mGestureManager;
+ }
+
+ /**
+ * Returns the current {@link HoverOverlay} painting the mouse hover.
+ *
+ * @return the hover overlay responsible for painting the mouse hover,
+ * never null
+ */
+ HoverOverlay getHoverOverlay() {
+ return mHoverOverlay;
+ }
+
+ /**
+ * Returns the horizontal {@link CanvasTransform} transform object, which can map
+ * a layout point into a control point.
+ *
+ * @return A {@link CanvasTransform} for mapping between layout and control
+ * coordinates in the horizontal dimension.
+ */
+ CanvasTransform getHorizontalTransform() {
+ return mHScale;
+ }
+
+ /**
+ * Returns the vertical {@link CanvasTransform} transform object, which can map a
+ * layout point into a control point.
+ *
+ * @return A {@link CanvasTransform} for mapping between layout and control
+ * coordinates in the vertical dimension.
+ */
+ CanvasTransform getVerticalTransform() {
+ return mVScale;
+ }
+
+ /**
+ * Returns the {@link OutlinePage} associated with this canvas
+ *
+ * @return the {@link OutlinePage} associated with this canvas
+ */
+ public OutlinePage getOutlinePage() {
+ return mOutlinePage;
+ }
+
+ /**
+ * Returns the {@link SelectionManager} associated with this canvas.
+ *
+ * @return The {@link SelectionManager} holding the selection for this
+ * canvas. Never null.
+ */
+ public SelectionManager getSelectionManager() {
+ return mSelectionManager;
+ }
+
+ /**
+ * Returns the {@link ViewHierarchy} object associated with this canvas,
+ * holding the most recent rendered view of the scene, if valid.
+ *
+ * @return The {@link ViewHierarchy} object associated with this canvas.
+ * Never null.
+ */
+ public ViewHierarchy getViewHierarchy() {
+ return mViewHierarchy;
+ }
+
+ /**
+ * Returns the {@link ClipboardSupport} object associated with this canvas.
+ *
+ * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose.
+ */
+ public ClipboardSupport getClipboardSupport() {
+ return mClipboardSupport;
+ }
+
+ /** Returns the Select All action bound to this canvas */
+ Action getSelectAllAction() {
+ return mSelectAllAction;
+ }
+
+ /** Returns the associated {@link GraphicalEditorPart} */
+ GraphicalEditorPart getGraphicalEditor() {
+ return mEditorDelegate.getGraphicalEditor();
+ }
+
+ /**
+ * Sets the result of the layout rendering. The result object indicates if the layout
+ * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
+ *
+ * Implementation detail: the bridge's computeLayout() method already returns a newly
+ * allocated ILayourResult. That means we can keep this result and hold on to it
+ * when it is valid.
+ *
+ * @param session The new scene, either valid or not.
+ * @param explodedNodes The set of individual nodes the layout computer was asked to
+ * explode. Note that these are independent of the explode-all mode where
+ * all views are exploded; this is used only for the mode (
+ * {@link #showInvisibleViews(boolean)}) where individual invisible nodes
+ * are padded during certain interactions.
+ */
+ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
+ boolean layoutlib5) {
+ // disable any hover
+ clearHover();
+
+ mViewHierarchy.setSession(session, explodedNodes, layoutlib5);
+ if (mViewHierarchy.isValid() && session != null) {
+ Image image = mImageOverlay.setImage(session.getImage(),
+ session.isAlphaChannelImage());
+
+ mOutlinePage.setModel(mViewHierarchy.getRoot());
+ getGraphicalEditor().setModel(mViewHierarchy.getRoot());
+
+ if (image != null) {
+ updateScrollBars();
+ if (mZoomFitNextImage) {
+ // Must be run asynchronously because getClientArea() returns 0 bounds
+ // when the editor is being initialized
+ getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!isDisposed()) {
+ ensureZoomed();
+ }
+ }
+ });
+ }
+
+ // Ensure that if we have a a preview mode enabled, it's shown
+ syncPreviewMode();
+ }
+ }
+
+ redraw();
+ }
+
+ void ensureZoomed() {
+ if (mZoomFitNextImage && getClientArea().height > 0) {
+ mZoomFitNextImage = false;
+ LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar();
+ if (actionBar.isZoomingAllowed()) {
+ setFitScale(true, true /*allowZoomIn*/);
+ }
+ }
+ }
+
+ void setShowOutline(boolean newState) {
+ mShowOutline = newState;
+ redraw();
+ }
+
+ /**
+ * Returns the zoom scale factor of the canvas (the amount the full
+ * resolution render of the device is zoomed before being shown on the
+ * canvas)
+ *
+ * @return the image scale
+ */
+ public double getScale() {
+ return mHScale.getScale();
+ }
+
+ void setScale(double scale, boolean redraw) {
+ if (scale <= 0.0) {
+ scale = 1.0;
+ }
+
+ if (scale == getScale()) {
+ return;
+ }
+
+ mHScale.setScale(scale);
+ mVScale.setScale(scale);
+ if (redraw) {
+ redraw();
+ }
+
+ // Clear the zoom setting if it is almost identical to 1.0
+ String zoomValue = (Math.abs(scale - 1.0) < 0.0001) ? null : Double.toString(scale);
+ IFile file = mEditorDelegate.getEditor().getInputFile();
+ if (file != null) {
+ AdtPlugin.setFileProperty(file, NAME_ZOOM, zoomValue);
+ }
+ }
+
+ /**
+ * Scales the canvas to best fit
+ *
+ * @param onlyZoomOut if true, then the zooming factor will never be larger than 1,
+ * which means that this function will zoom out if necessary to show the
+ * rendered image, but it will never zoom in.
+ * TODO: Rename this, it sounds like it conflicts with allowZoomIn,
+ * even though one is referring to the zoom level and one is referring
+ * to the overall act of scaling above/below 1.
+ * @param allowZoomIn if false, then if the computed zoom factor is smaller than
+ * the current zoom factor, it will be ignored.
+ */
+ public void setFitScale(boolean onlyZoomOut, boolean allowZoomIn) {
+ ImageOverlay imageOverlay = getImageOverlay();
+ if (imageOverlay == null) {
+ return;
+ }
+ Image image = imageOverlay.getImage();
+ if (image != null) {
+ Rectangle canvasSize = getClientArea();
+ int canvasWidth = canvasSize.width;
+ int canvasHeight = canvasSize.height;
+
+ boolean hasPreviews = mPreviewManager.hasPreviews();
+ if (hasPreviews) {
+ canvasWidth = 2 * canvasWidth / 3;
+ } else {
+ canvasWidth -= 4;
+ canvasHeight -= 4;
+ }
+
+ ImageData imageData = image.getImageData();
+ int sceneWidth = imageData.width;
+ int sceneHeight = imageData.height;
+ if (sceneWidth == 0.0 || sceneHeight == 0.0) {
+ return;
+ }
+
+ if (imageOverlay.getShowDropShadow()) {
+ sceneWidth += 2 * ImageUtils.SHADOW_SIZE;
+ sceneHeight += 2 * ImageUtils.SHADOW_SIZE;
+ }
+
+ // Reduce the margins if necessary
+ int hDelta = canvasWidth - sceneWidth;
+ int hMargin = 0;
+ if (hDelta > 2 * CanvasTransform.DEFAULT_MARGIN) {
+ hMargin = CanvasTransform.DEFAULT_MARGIN;
+ } else if (hDelta > 0) {
+ hMargin = hDelta / 2;
+ }
+
+ int vDelta = canvasHeight - sceneHeight;
+ int vMargin = 0;
+ if (vDelta > 2 * CanvasTransform.DEFAULT_MARGIN) {
+ vMargin = CanvasTransform.DEFAULT_MARGIN;
+ } else if (vDelta > 0) {
+ vMargin = vDelta / 2;
+ }
+
+ double hScale = (canvasWidth - 2 * hMargin) / (double) sceneWidth;
+ double vScale = (canvasHeight - 2 * vMargin) / (double) sceneHeight;
+
+ double scale = Math.min(hScale, vScale);
+
+ if (onlyZoomOut) {
+ scale = Math.min(1.0, scale);
+ }
+
+ if (!allowZoomIn && scale > getScale()) {
+ return;
+ }
+
+ setScale(scale, true);
+ }
+ }
+
+ /**
+ * Transforms a point, expressed in layout coordinates, into "client" coordinates
+ * relative to the control (and not relative to the display).
+ *
+ * @param canvasX X in the canvas coordinates
+ * @param canvasY Y in the canvas coordinates
+ * @return A new {@link Point} in control client coordinates (not display coordinates)
+ */
+ Point layoutToControlPoint(int canvasX, int canvasY) {
+ int x = mHScale.translate(canvasX);
+ int y = mVScale.translate(canvasY);
+ return new Point(x, y);
+ }
+
+ /**
+ * Returns the action for the context menu corresponding to the given action id.
+ * <p/>
+ * For global actions such as copy or paste, the action id must be composed of
+ * the {@link #PREFIX_CANVAS_ACTION} followed by one of {@link ActionFactory}'s
+ * action ids.
+ * <p/>
+ * Returns null if there's no action for the given id.
+ */
+ IAction getAction(String actionId) {
+ String prefix = PREFIX_CANVAS_ACTION;
+ if (mMenuManager == null ||
+ actionId == null ||
+ !actionId.startsWith(prefix)) {
+ return null;
+ }
+
+ actionId = actionId.substring(prefix.length());
+
+ for (IContributionItem contrib : mMenuManager.getItems()) {
+ if (contrib instanceof ActionContributionItem &&
+ actionId.equals(contrib.getId())) {
+ return ((ActionContributionItem) contrib).getAction();
+ }
+ }
+
+ return null;
+ }
+
+ //---------------
+
+ /**
+ * Paints the canvas in response to paint events.
+ */
+ private void onPaint(PaintEvent e) {
+ GC gc = e.gc;
+ gc.setFont(mFont);
+ mGCWrapper.setGC(gc);
+ try {
+ if (!mImageOverlay.isHiding()) {
+ mImageOverlay.paint(gc);
+ }
+
+ mPreviewManager.paint(gc);
+
+ if (mShowOutline) {
+ if (mOutlineOverlay == null) {
+ mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale);
+ mOutlineOverlay.create(getDisplay());
+ }
+ if (!mOutlineOverlay.isHiding()) {
+ mOutlineOverlay.paint(gc);
+ }
+ }
+
+ if (mShowInvisible) {
+ if (mEmptyOverlay == null) {
+ mEmptyOverlay = new EmptyViewsOverlay(mViewHierarchy, mHScale, mVScale);
+ mEmptyOverlay.create(getDisplay());
+ }
+ if (!mEmptyOverlay.isHiding()) {
+ mEmptyOverlay.paint(gc);
+ }
+ }
+
+ if (!mHoverOverlay.isHiding()) {
+ mHoverOverlay.paint(gc);
+ }
+
+ if (!mLintOverlay.isHiding()) {
+ mLintOverlay.paint(gc);
+ }
+
+ if (!mIncludeOverlay.isHiding()) {
+ mIncludeOverlay.paint(gc);
+ }
+
+ if (!mSelectionOverlay.isHiding()) {
+ mSelectionOverlay.paint(mSelectionManager, mGCWrapper, gc, mRulesEngine);
+ }
+ mGestureManager.paint(gc);
+
+ } finally {
+ mGCWrapper.setGC(null);
+ }
+ }
+
+ /**
+ * Shows or hides invisible parent views, which are views which have empty bounds and
+ * no children. The nodes which will be shown are provided by
+ * {@link #getNodesToExplode()}.
+ *
+ * @param show When true, any invisible parent nodes are padded and highlighted
+ * ("exploded"), and when false any formerly exploded nodes are hidden.
+ */
+ void showInvisibleViews(boolean show) {
+ if (mShowInvisible == show) {
+ return;
+ }
+ mShowInvisible = show;
+
+ // Optimization: Avoid doing work when we don't have invisible parents (on show)
+ // or formerly exploded nodes (on hide).
+ if (show && !mViewHierarchy.hasInvisibleParents()) {
+ return;
+ } else if (!show && !mViewHierarchy.hasExplodedParents()) {
+ return;
+ }
+
+ mEditorDelegate.recomputeLayout();
+ }
+
+ /**
+ * Returns a set of nodes that should be exploded (forced non-zero padding during render),
+ * or null if no nodes should be exploded. (Note that this is independent of the
+ * explode-all mode, where all nodes are padded -- that facility does not use this
+ * mechanism, which is only intended to be used to expose invisible parent nodes.
+ *
+ * @return The set of invisible parents, or null if no views should be expanded.
+ */
+ public Set<UiElementNode> getNodesToExplode() {
+ if (mShowInvisible) {
+ return mViewHierarchy.getInvisibleNodes();
+ }
+
+ // IF we have selection, and IF we have invisible nodes in the view,
+ // see if any of the selected items are among the invisible nodes, and if so
+ // add them to a lazily constructed set which we pass back for rendering.
+ Set<UiElementNode> result = null;
+ List<SelectionItem> selections = mSelectionManager.getSelections();
+ if (selections.size() > 0) {
+ List<CanvasViewInfo> invisibleParents = mViewHierarchy.getInvisibleViews();
+ if (invisibleParents.size() > 0) {
+ for (SelectionItem item : selections) {
+ CanvasViewInfo viewInfo = item.getViewInfo();
+ // O(n^2) here, but both the selection size and especially the
+ // invisibleParents size are expected to be small
+ if (invisibleParents.contains(viewInfo)) {
+ UiViewElementNode node = viewInfo.getUiViewNode();
+ if (node != null) {
+ if (result == null) {
+ result = new HashSet<UiElementNode>();
+ }
+ result.add(node);
+ }
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Clears the hover.
+ */
+ void clearHover() {
+ mHoverOverlay.clearHover();
+ }
+
+ /**
+ * Hover on top of a known child.
+ */
+ void hover(MouseEvent e) {
+ // Check if a button is pressed; no hovers during drags
+ if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+ clearHover();
+ return;
+ }
+
+ LayoutPoint p = ControlPoint.create(this, e).toLayout();
+ CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);
+
+ // We don't hover on the root since it's not a widget per see and it is always there.
+ // We also skip spacers...
+ if (vi != null && (vi.isRoot() || vi.isHidden())) {
+ vi = null;
+ }
+
+ boolean needsUpdate = vi != mHoverViewInfo;
+ mHoverViewInfo = vi;
+
+ if (vi == null) {
+ clearHover();
+ } else {
+ Rectangle r = vi.getSelectionRect();
+ mHoverOverlay.setHover(r.x, r.y, r.width, r.height);
+ }
+
+ if (needsUpdate) {
+ redraw();
+ }
+ }
+
+ /**
+ * Shows the given {@link CanvasViewInfo}, which can mean exposing its XML or if it's
+ * an included element, its corresponding file.
+ *
+ * @param vi the {@link CanvasViewInfo} to be shown
+ */
+ public void show(CanvasViewInfo vi) {
+ String url = vi.getIncludeUrl();
+ if (url != null) {
+ showInclude(url);
+ } else {
+ showXml(vi);
+ }
+ }
+
+ /**
+ * Shows the layout file referenced by the given url in the same project.
+ *
+ * @param url The layout attribute url of the form @layout/foo
+ */
+ private void showInclude(String url) {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ IPath filePath = graphicalEditor.findResourceFile(url);
+ if (filePath == null) {
+ // Should not be possible - if the URL had been bad, then we wouldn't
+ // have been able to render the scene and you wouldn't have been able
+ // to click on it
+ return;
+ }
+
+ // Save the including file, if necessary: without it, the "Show Included In"
+ // facility which is invoked automatically will not work properly if the <include>
+ // tag is not in the saved version of the file, since the outer file is read from
+ // disk rather than from memory.
+ IEditorSite editorSite = graphicalEditor.getEditorSite();
+ IWorkbenchPage page = editorSite.getPage();
+ page.saveEditor(mEditorDelegate.getEditor(), false);
+
+ IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
+ IFile xmlFile = null;
+ IPath workspacePath = workspace.getLocation();
+ if (workspacePath.isPrefixOf(filePath)) {
+ IPath relativePath = filePath.makeRelativeTo(workspacePath);
+ xmlFile = (IFile) workspace.findMember(relativePath);
+ } else if (filePath.isAbsolute()) {
+ xmlFile = workspace.getFileForLocation(filePath);
+ }
+ if (xmlFile != null) {
+ IFile leavingFile = graphicalEditor.getEditedFile();
+ Reference next = Reference.create(graphicalEditor.getEditedFile());
+
+ try {
+ IEditorPart openAlready = EditorUtility.isOpenInEditor(xmlFile);
+
+ // Show the included file as included within this click source?
+ if (openAlready != null) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(openAlready);
+ if (delegate != null) {
+ GraphicalEditorPart gEditor = delegate.getGraphicalEditor();
+ if (gEditor != null &&
+ gEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ gEditor.showIn(next);
+ }
+ }
+ } else {
+ try {
+ // Set initial state of a new file
+ // TODO: Only set rendering target portion of the state
+ String state = ConfigurationDescription.getDescription(leavingFile);
+ xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE,
+ state);
+ } catch (CoreException e) {
+ // pass
+ }
+
+ if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ try {
+ xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, next);
+ } catch (CoreException e) {
+ // pass - worst that can happen is that we don't
+ //start with inclusion
+ }
+ }
+ }
+
+ EditorUtility.openInEditor(xmlFile, true);
+ return;
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
+ }
+ } else {
+ // It's not a path in the workspace; look externally
+ // (this is probably an @android: path)
+ if (filePath.isAbsolute()) {
+ IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath);
+ // fileStore = fileStore.getChild(names[i]);
+ if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) {
+ try {
+ IDE.openEditorOnFileStore(page, fileStore);
+ return;
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ // Failed: display message to the user
+ String message = String.format("Could not find resource %1$s", url);
+ IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
+ status.setErrorMessage(message);
+ getDisplay().beep();
+ }
+
+ /**
+ * Returns the layout resource name of this layout
+ *
+ * @return the layout resource name of this layout
+ */
+ public String getLayoutResourceName() {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ return graphicalEditor.getLayoutResourceName();
+ }
+
+ /**
+ * Returns the layout resource url of the current layout
+ *
+ * @return
+ */
+ /*
+ public String getMe() {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ IFile editedFile = graphicalEditor.getEditedFile();
+ return editedFile.getProjectRelativePath().toOSString();
+ }
+ */
+
+ /**
+ * Show the XML element corresponding to the given {@link CanvasViewInfo} (unless it's
+ * a root).
+ *
+ * @param vi The clicked {@link CanvasViewInfo} whose underlying XML element we want
+ * to view
+ */
+ private void showXml(CanvasViewInfo vi) {
+ // Warp to the text editor and show the corresponding XML for the
+ // double-clicked widget
+ if (vi.isRoot()) {
+ return;
+ }
+
+ Node xmlNode = vi.getXmlNode();
+ if (xmlNode != null) {
+ boolean found = mEditorDelegate.getEditor().show(xmlNode);
+ if (!found) {
+ getDisplay().beep();
+ }
+ }
+ }
+
+ //---------------
+
+ /**
+ * Helper to create the drag source for the given control.
+ * <p/>
+ * This is static with package-access so that {@link OutlinePage} can also
+ * create an exact copy of the source with the same attributes.
+ */
+ /* package */static DragSource createDragSource(Control control) {
+ DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
+ source.setTransfer(new Transfer[] {
+ TextTransfer.getInstance(),
+ SimpleXmlTransfer.getInstance()
+ });
+ return source;
+ }
+
+ /**
+ * Helper to create the drop target for the given control.
+ */
+ private static DropTarget createDropTarget(Control control) {
+ DropTarget dropTarget = new DropTarget(
+ control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
+ dropTarget.setTransfer(new Transfer[] {
+ SimpleXmlTransfer.getInstance()
+ });
+ return dropTarget;
+ }
+
+ //---------------
+
+ /**
+ * Invoked by the constructor to add our cut/copy/paste/delete/select-all
+ * handlers in the global action handlers of this editor's site.
+ * <p/>
+ * This will enable the menu items under the global Edit menu and make them
+ * invoke our actions as needed. As a benefit, the corresponding shortcut
+ * accelerators will do what one would expect.
+ */
+ private void setupGlobalActionHandlers() {
+ mCutAction = new Action() {
+ @Override
+ public void run() {
+ mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot());
+ updateMenuActionState();
+ }
+ };
+
+ copyActionAttributes(mCutAction, ActionFactory.CUT);
+
+ mCopyAction = new Action() {
+ @Override
+ public void run() {
+ mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot());
+ updateMenuActionState();
+ }
+ };
+
+ copyActionAttributes(mCopyAction, ActionFactory.COPY);
+
+ mPasteAction = new Action() {
+ @Override
+ public void run() {
+ mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot());
+ updateMenuActionState();
+ }
+ };
+
+ copyActionAttributes(mPasteAction, ActionFactory.PASTE);
+
+ mDeleteAction = new Action() {
+ @Override
+ public void run() {
+ mClipboardSupport.deleteSelection(
+ getDeleteLabel(),
+ mSelectionManager.getSnapshot());
+ }
+ };
+
+ copyActionAttributes(mDeleteAction, ActionFactory.DELETE);
+
+ mSelectAllAction = new Action() {
+ @Override
+ public void run() {
+ GraphicalEditorPart graphicalEditor = getEditorDelegate().getGraphicalEditor();
+ StyledText errorLabel = graphicalEditor.getErrorLabel();
+ if (errorLabel.isFocusControl()) {
+ errorLabel.selectAll();
+ return;
+ }
+
+ mSelectionManager.selectAll();
+ }
+ };
+
+ copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL);
+ }
+
+ String getCutLabel() {
+ return mCutAction.getText();
+ }
+
+ String getDeleteLabel() {
+ // verb "Delete" from the DELETE action's title
+ return mDeleteAction.getText();
+ }
+
+ /**
+ * Updates menu actions that depends on the selection.
+ */
+ void updateMenuActionState() {
+ List<SelectionItem> selections = getSelectionManager().getSelections();
+ boolean hasSelection = !selections.isEmpty();
+ if (hasSelection && selections.size() == 1 && selections.get(0).isRoot()) {
+ hasSelection = false;
+ }
+
+ StyledText errorLabel = getGraphicalEditor().getErrorLabel();
+ mCutAction.setEnabled(hasSelection);
+ mCopyAction.setEnabled(hasSelection || errorLabel.getSelectionCount() > 0);
+ mDeleteAction.setEnabled(hasSelection);
+ // Select All should *always* be selectable, regardless of whether anything
+ // is currently selected.
+ mSelectAllAction.setEnabled(true);
+
+ // The paste operation is only available if we can paste our custom type.
+ // We do not currently support pasting random text (e.g. XML). Maybe later.
+ boolean hasSxt = mClipboardSupport.hasSxtOnClipboard();
+ mPasteAction.setEnabled(hasSxt);
+ }
+
+ /**
+ * Update the actions when this editor is activated
+ *
+ * @param bars the action bar for this canvas
+ */
+ public void updateGlobalActions(@NonNull IActionBars bars) {
+ updateMenuActionState();
+
+ ITextEditor editor = mEditorDelegate.getEditor().getStructuredTextEditor();
+ boolean graphical = getEditorDelegate().getEditor().getActivePage() == 0;
+ if (graphical) {
+ bars.setGlobalActionHandler(ActionFactory.CUT.getId(), mCutAction);
+ bars.setGlobalActionHandler(ActionFactory.COPY.getId(), mCopyAction);
+ bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), mPasteAction);
+ bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), mDeleteAction);
+ bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), mSelectAllAction);
+
+ // Delegate the Undo and Redo actions to the text editor ones, but wrap them
+ // such that we run lint to update the results on the current page (this is
+ // normally done on each editor operation that goes through
+ // {@link AndroidXmlEditor#wrapUndoEditXmlModel}, but not undo/redo)
+ if (mUndoAction == null) {
+ IAction undoAction = editor.getAction(ActionFactory.UNDO.getId());
+ mUndoAction = new LintEditAction(undoAction, getEditorDelegate().getEditor());
+ }
+ bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), mUndoAction);
+ if (mRedoAction == null) {
+ IAction redoAction = editor.getAction(ActionFactory.REDO.getId());
+ mRedoAction = new LintEditAction(redoAction, getEditorDelegate().getEditor());
+ }
+ bars.setGlobalActionHandler(ActionFactory.REDO.getId(), mRedoAction);
+ } else {
+ bars.setGlobalActionHandler(ActionFactory.CUT.getId(),
+ editor.getAction(ActionFactory.CUT.getId()));
+ bars.setGlobalActionHandler(ActionFactory.COPY.getId(),
+ editor.getAction(ActionFactory.COPY.getId()));
+ bars.setGlobalActionHandler(ActionFactory.PASTE.getId(),
+ editor.getAction(ActionFactory.PASTE.getId()));
+ bars.setGlobalActionHandler(ActionFactory.DELETE.getId(),
+ editor.getAction(ActionFactory.DELETE.getId()));
+ bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ editor.getAction(ActionFactory.SELECT_ALL.getId()));
+ bars.setGlobalActionHandler(ActionFactory.UNDO.getId(),
+ editor.getAction(ActionFactory.UNDO.getId()));
+ bars.setGlobalActionHandler(ActionFactory.REDO.getId(),
+ editor.getAction(ActionFactory.REDO.getId()));
+ }
+
+ bars.updateActionBars();
+ }
+
+ /**
+ * Helper for {@link #setupGlobalActionHandlers()}.
+ * Copies the action attributes form the given {@link ActionFactory}'s action to
+ * our action.
+ * <p/>
+ * {@link ActionFactory} provides access to the standard global actions in Eclipse.
+ * <p/>
+ * This allows us to grab the standard labels and icons for the
+ * global actions such as copy, cut, paste, delete and select-all.
+ */
+ private void copyActionAttributes(Action action, ActionFactory factory) {
+ IWorkbenchAction wa = factory.create(
+ mEditorDelegate.getEditor().getEditorSite().getWorkbenchWindow());
+ action.setId(wa.getId());
+ action.setText(wa.getText());
+ action.setEnabled(wa.isEnabled());
+ action.setDescription(wa.getDescription());
+ action.setToolTipText(wa.getToolTipText());
+ action.setAccelerator(wa.getAccelerator());
+ action.setActionDefinitionId(wa.getActionDefinitionId());
+ action.setImageDescriptor(wa.getImageDescriptor());
+ action.setHoverImageDescriptor(wa.getHoverImageDescriptor());
+ action.setDisabledImageDescriptor(wa.getDisabledImageDescriptor());
+ action.setHelpListener(wa.getHelpListener());
+ }
+
+ /**
+ * Creates the context menu for the canvas. This is called once from the canvas' constructor.
+ * <p/>
+ * The menu has a static part with actions that are always available such as
+ * copy, cut, paste and show in > explorer. This is created by
+ * {@link #setupStaticMenuActions(IMenuManager)}.
+ * <p/>
+ * There's also a dynamic part that is populated by the rules of the
+ * selected elements, created by {@link DynamicContextMenu}.
+ */
+ @SuppressWarnings("unused")
+ private void createContextMenu() {
+
+ // This manager is the root of the context menu.
+ mMenuManager = new MenuManager() {
+ @Override
+ public boolean isDynamic() {
+ return true;
+ }
+ };
+
+ // Fill the menu manager with the static & dynamic actions
+ setupStaticMenuActions(mMenuManager);
+ new DynamicContextMenu(mEditorDelegate, this, mMenuManager);
+ Menu menu = mMenuManager.createContextMenu(this);
+ setMenu(menu);
+
+ // Add listener to detect when the menu is about to be posted, such that
+ // we can sync the selection. Without this, you can right click on something
+ // in the canvas which is NOT selected, and the context menu will show items related
+ // to the selection, NOT the item you clicked on!!
+ addMenuDetectListener(new MenuDetectListener() {
+ @Override
+ public void menuDetected(MenuDetectEvent e) {
+ mSelectionManager.menuClick(e);
+ }
+ });
+ }
+
+ /**
+ * Invoked by {@link #createContextMenu()} to create our *static* context menu once.
+ * <p/>
+ * The content of the menu itself does not change. However the state of the
+ * various items is controlled by their associated actions.
+ * <p/>
+ * For cut/copy/paste/delete/select-all, we explicitly reuse the actions
+ * created by {@link #setupGlobalActionHandlers()}, so this method must be
+ * invoked after that one.
+ */
+ private void setupStaticMenuActions(IMenuManager manager) {
+ manager.removeAll();
+
+ manager.add(new SelectionManager.SelectionMenu(getGraphicalEditor()));
+ manager.add(new Separator());
+ manager.add(mCutAction);
+ manager.add(mCopyAction);
+ manager.add(mPasteAction);
+ manager.add(new Separator());
+ manager.add(mDeleteAction);
+ manager.add(new Separator());
+ manager.add(new PlayAnimationMenu(this));
+ manager.add(new ExportScreenshotAction(this));
+ manager.add(new Separator());
+
+ // Group "Show Included In" and "Show In" together
+ manager.add(new ShowWithinMenu(mEditorDelegate));
+
+ // Create a "Show In" sub-menu and automatically populate it using standard
+ // actions contributed by the workbench.
+ String showInLabel = IDEWorkbenchMessages.Workbench_showIn;
+ MenuManager showInSubMenu = new MenuManager(showInLabel);
+ showInSubMenu.add(
+ ContributionItemFactory.VIEWS_SHOW_IN.create(
+ mEditorDelegate.getEditor().getSite().getWorkbenchWindow()));
+ manager.add(showInSubMenu);
+ }
+
+ /**
+ * Deletes the selection. Equivalent to pressing the Delete key.
+ */
+ void delete() {
+ mDeleteAction.run();
+ }
+
+ /**
+ * Add new root in an existing empty XML layout.
+ * <p/>
+ * In case of error (unknown FQCN, document not empty), silently do nothing.
+ * In case of success, the new element will have some default attributes set
+ * (xmlns:android, layout_width and height). The edit is wrapped in a proper
+ * undo.
+ * <p/>
+ * This is invoked by
+ * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
+ *
+ * @param root A non-null descriptor of the root element to create.
+ */
+ void createDocumentRoot(final @NonNull SimpleElement root) {
+ String rootFqcn = root.getFqcn();
+
+ // Need a valid empty document to create the new root
+ final UiDocumentNode uiDoc = mEditorDelegate.getUiRootNode();
+ if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
+ debugPrintf("Failed to create document root for %1$s: document is not empty",
+ rootFqcn);
+ return;
+ }
+
+ // Find the view descriptor matching our FQCN
+ final ViewElementDescriptor viewDesc = mEditorDelegate.getFqcnViewDescriptor(rootFqcn);
+ if (viewDesc == null) {
+ // TODO this could happen if dropping a custom view not known in this project
+ debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn);
+ return;
+ }
+
+ // Get the last segment of the FQCN for the undo title
+ String title = rootFqcn;
+ int pos = title.lastIndexOf('.');
+ if (pos > 0 && pos < title.length() - 1) {
+ title = title.substring(pos + 1);
+ }
+ title = String.format("Create root %1$s in document", title);
+
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() {
+ @Override
+ public void run() {
+ UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
+
+ // A root node requires the Android XMLNS
+ uiNew.setAttributeValue(
+ SdkConstants.ANDROID_NS_NAME,
+ SdkConstants.XMLNS_URI,
+ SdkConstants.NS_RESOURCES,
+ true /*override*/);
+
+ IDragAttribute[] attributes = root.getAttributes();
+ if (attributes != null) {
+ for (IDragAttribute attribute : attributes) {
+ String uri = attribute.getUri();
+ String name = attribute.getName();
+ String value = attribute.getValue();
+ uiNew.setAttributeValue(name, uri, value, false /*override*/);
+ }
+ }
+
+ // Adjust the attributes
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
+
+ uiNew.createXmlNode();
+ }
+ });
+ }
+
+ /**
+ * Returns the insets associated with views of the given fully qualified name, for the
+ * current theme and screen type.
+ *
+ * @param fqcn the fully qualified name to the widget type
+ * @return the insets, or null if unknown
+ */
+ public Margins getInsets(String fqcn) {
+ if (ViewMetadataRepository.INSETS_SUPPORTED) {
+ ConfigurationChooser configComposite = getGraphicalEditor().getConfigurationChooser();
+ String theme = configComposite.getThemeName();
+ Density density = configComposite.getConfiguration().getDensity();
+ return ViewMetadataRepository.getInsets(fqcn, density, theme);
+ } else {
+ return null;
+ }
+ }
+
+ private void debugPrintf(String message, Object... params) {
+ if (DEBUG) {
+ AdtPlugin.printToConsole("Canvas", String.format(message, params));
+ }
+ }
+
+ /** The associated editor has been deactivated */
+ public void deactivated() {
+ // Force the tooltip to be hidden. If you switch from the layout editor
+ // to a Java editor with the keyboard, the tooltip can stay open.
+ if (mLintTooltipManager != null) {
+ mLintTooltipManager.hide();
+ }
+ }
+
+ /** @see #setPreview(RenderPreview) */
+ private RenderPreview mPreview;
+
+ /**
+ * Sets the {@link RenderPreview} associated with the currently rendering
+ * configuration.
+ * <p>
+ * A {@link RenderPreview} has various additional state beyond its rendering,
+ * such as its display name (which can be edited by the user). When you click on
+ * previews, the layout editor switches to show the given configuration preview.
+ * The preview is then no longer shown in the list of previews and is instead rendered
+ * in the main editor. However, when you then switch away to some other preview, we
+ * want to be able to restore the preview with all its state.
+ *
+ * @param preview the preview associated with the current canvas
+ */
+ public void setPreview(@Nullable RenderPreview preview) {
+ mPreview = preview;
+ }
+
+ /**
+ * Returns the {@link RenderPreview} associated with this layout canvas.
+ *
+ * @see #setPreview(RenderPreview)
+ * @return the {@link RenderPreview}
+ */
+ @Nullable
+ public RenderPreview getPreview() {
+ return mPreview;
+ }
+
+ /** Ensures that the configuration previews are up to date for this canvas */
+ public void syncPreviewMode() {
+ if (mImageOverlay != null && mImageOverlay.getImage() != null &&
+ getGraphicalEditor().getConfigurationChooser().getResources() != null) {
+ if (mPreviewManager.recomputePreviews(false)) {
+ // Zoom when syncing modes
+ mZoomFitNextImage = true;
+ ensureZoomed();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java
new file mode 100644
index 000000000..e349a1cb0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.IPostSelectionProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+
+/**
+ * JFace {@link Viewer} wrapper around {@link LayoutCanvas}.
+ * <p/>
+ * The viewer is owned by {@link GraphicalEditorPart}.
+ * <p/>
+ * The viewer is an {@link ISelectionProvider} instance and is set as the
+ * site's main {@link ISelectionProvider} by the editor part. Consequently
+ * canvas' selection changes are broadcasted to anyone listening, which includes
+ * the part itself as well as the associated outline and property sheet pages.
+ */
+class LayoutCanvasViewer extends Viewer implements IPostSelectionProvider {
+
+ private LayoutCanvas mCanvas;
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ public LayoutCanvasViewer(LayoutEditorDelegate editorDelegate,
+ RulesEngine rulesEngine,
+ Composite parent,
+ int style) {
+ mEditorDelegate = editorDelegate;
+ mCanvas = new LayoutCanvas(editorDelegate, rulesEngine, parent, style);
+
+ mCanvas.getSelectionManager().addSelectionChangedListener(mSelectionListener);
+ }
+
+ private ISelectionChangedListener mSelectionListener = new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ fireSelectionChanged(event);
+ firePostSelectionChanged(event);
+ }
+ };
+
+ @Override
+ public Control getControl() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the underlying {@link LayoutCanvas}.
+ * This is the same control as returned by {@link #getControl()} but clients
+ * have it already casted in the right type.
+ * <p/>
+ * This can never be null.
+ * @return The underlying {@link LayoutCanvas}.
+ */
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the current layout editor's input.
+ */
+ @Override
+ public Object getInput() {
+ return mEditorDelegate.getEditor().getEditorInput();
+ }
+
+ /**
+ * Unused. We don't support switching the input.
+ */
+ @Override
+ public void setInput(Object input) {
+ }
+
+ /**
+ * Returns a new {@link TreeSelection} where each {@link TreePath} item
+ * is a {@link CanvasViewInfo}.
+ */
+ @Override
+ public ISelection getSelection() {
+ return mCanvas.getSelectionManager().getSelection();
+ }
+
+ /**
+ * Sets a new selection. <code>reveal</code> is ignored right now.
+ * <p/>
+ * The selection can be null, which is interpreted as an empty selection.
+ */
+ @Override
+ public void setSelection(ISelection selection, boolean reveal) {
+ if (mEditorDelegate.getEditor().getIgnoreXmlUpdate()) {
+ return;
+ }
+ mCanvas.getSelectionManager().setSelection(selection);
+ }
+
+ /** Unused. Refreshing is done solely by the owning {@link LayoutEditorDelegate}. */
+ @Override
+ public void refresh() {
+ // ignore
+ }
+
+ public void dispose() {
+ if (mSelectionListener != null) {
+ mCanvas.getSelectionManager().removeSelectionChangedListener(mSelectionListener);
+ }
+ if (mCanvas != null) {
+ mCanvas.dispose();
+ mCanvas = null;
+ }
+ }
+
+ // ---- Implements IPostSelectionProvider ----
+
+ private ListenerList mPostChangedListeners = new ListenerList();
+
+ @Override
+ public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
+ mPostChangedListeners.add(listener);
+ }
+
+ @Override
+ public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
+ mPostChangedListeners.remove(listener);
+ }
+
+ protected void firePostSelectionChanged(final SelectionChangedEvent event) {
+ Object[] listeners = mPostChangedListeners.getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final ISelectionChangedListener l = (ISelectionChangedListener) listeners[i];
+ SafeRunnable.run(new SafeRunnable() {
+ @Override
+ public void run() {
+ l.selectionChanged(event);
+ }
+ });
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java
new file mode 100644
index 000000000..b79e3b0a1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_NUM_COLUMNS;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VALUE_AUTO_FIT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.AdapterBinding;
+import com.android.ide.common.rendering.api.DataBindingItem;
+import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.progress.WorkbenchJob;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings.
+ */
+public class LayoutMetadata {
+ /** The default layout to use for list items in expandable list views */
+ public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$
+ /** The default layout to use for list items in plain list views */
+ public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$
+ /** The default layout to use for list items in spinners */
+ public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$
+
+ /** The string to start metadata comments with */
+ private static final String COMMENT_PROLOGUE = " Preview: ";
+ /** The property key, included in comments, which references a list item layout */
+ public static final String KEY_LV_ITEM = "listitem"; //$NON-NLS-1$
+ /** The property key, included in comments, which references a list header layout */
+ public static final String KEY_LV_HEADER = "listheader"; //$NON-NLS-1$
+ /** The property key, included in comments, which references a list footer layout */
+ public static final String KEY_LV_FOOTER = "listfooter"; //$NON-NLS-1$
+ /** The property key, included in comments, which references a fragment layout to show */
+ public static final String KEY_FRAGMENT_LAYOUT = "layout"; //$NON-NLS-1$
+ // NOTE: If you add additional keys related to resources, make sure you update the
+ // ResourceRenameParticipant
+
+ /** Utility class, do not create instances */
+ private LayoutMetadata() {
+ }
+
+ /**
+ * Returns the given property specified in the <b>current</b> element being
+ * processed by the given pull parser.
+ *
+ * @param parser the pull parser, which must be in the middle of processing
+ * the target element
+ * @param name the property name to look up
+ * @return the property value, or null if not defined
+ */
+ @Nullable
+ public static String getProperty(@NonNull XmlPullParser parser, @NonNull String name) {
+ String value = parser.getAttributeValue(TOOLS_URI, name);
+ if (value != null && value.isEmpty()) {
+ value = null;
+ }
+
+ return value;
+ }
+
+ /**
+ * Clears the old metadata from the given node
+ *
+ * @param node the XML node to associate metadata with
+ * @deprecated this method clears metadata using the old comment-based style;
+ * should only be used for migration at this point
+ */
+ @Deprecated
+ public static void clearLegacyComment(Node node) {
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.COMMENT_NODE) {
+ String text = child.getNodeValue();
+ if (text.startsWith(COMMENT_PROLOGUE)) {
+ Node commentNode = child;
+ // Remove the comment, along with surrounding whitespace if applicable
+ Node previous = commentNode.getPreviousSibling();
+ if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
+ if (previous.getNodeValue().trim().length() == 0) {
+ node.removeChild(previous);
+ }
+ }
+ node.removeChild(commentNode);
+ Node first = node.getFirstChild();
+ if (first != null && first.getNextSibling() == null
+ && first.getNodeType() == Node.TEXT_NODE) {
+ if (first.getNodeValue().trim().length() == 0) {
+ node.removeChild(first);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the given property of the given DOM node, or null
+ *
+ * @param node the XML node to associate metadata with
+ * @param name the name of the property to look up
+ * @return the value stored with the given node and name, or null
+ */
+ @Nullable
+ public static String getProperty(
+ @NonNull Node node,
+ @NonNull String name) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String value = element.getAttributeNS(TOOLS_URI, name);
+ if (value != null && value.isEmpty()) {
+ value = null;
+ }
+
+ return value;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the given property of the given DOM node to a given value, or if null clears
+ * the property.
+ *
+ * @param editor the editor associated with the property
+ * @param node the XML node to associate metadata with
+ * @param name the name of the property to set
+ * @param value the value to store for the given node and name, or null to remove it
+ */
+ public static void setProperty(
+ @NonNull final AndroidXmlEditor editor,
+ @NonNull final Node node,
+ @NonNull final String name,
+ @Nullable final String value) {
+ // Clear out the old metadata
+ clearLegacyComment(node);
+
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ final Element element = (Element) node;
+ final String undoLabel = "Bind View";
+ AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value,
+ false /*reveal*/, false /*append*/);
+
+ // Also apply the same layout to any corresponding elements in other configurations
+ // of this layout.
+ final IFile file = editor.getInputFile();
+ if (file != null) {
+ final List<IFile> variations = AdtUtils.getResourceVariations(file, false);
+ if (variations.isEmpty()) {
+ return;
+ }
+ Display display = AdtPlugin.getDisplay();
+ WorkbenchJob job = new WorkbenchJob(display, "Update alternate views") {
+ @Override
+ public IStatus runInUIThread(IProgressMonitor monitor) {
+ for (IFile variation : variations) {
+ if (variation.equals(file)) {
+ continue;
+ }
+ try {
+ // If the corresponding file is open in the IDE, use the
+ // editor version instead
+ if (!AdtPrefs.getPrefs().isSharedLayoutEditor()) {
+ if (setPropertyInEditor(undoLabel, variation, element, name,
+ value)) {
+ return Status.OK_STATUS;
+ }
+ }
+
+ boolean old = editor.getIgnoreXmlUpdate();
+ try {
+ editor.setIgnoreXmlUpdate(true);
+ setPropertyInFile(undoLabel, variation, element, name, value);
+ } finally {
+ editor.setIgnoreXmlUpdate(old);
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, variation.getFullPath().toOSString());
+ }
+ }
+ return Status.OK_STATUS;
+ }
+
+ };
+ job.setSystem(true);
+ job.schedule();
+ }
+ }
+ }
+
+ private static boolean setPropertyInEditor(
+ @NonNull String undoLabel,
+ @NonNull IFile variation,
+ @NonNull final Element equivalentElement,
+ @NonNull final String name,
+ @Nullable final String value) {
+ Collection<IEditorPart> editors =
+ AdtUtils.findEditorsFor(variation, false /*restore*/);
+ for (IEditorPart part : editors) {
+ AndroidXmlEditor editor = AdtUtils.getXmlEditor(part);
+ if (editor != null) {
+ Document doc = DomUtilities.getDocument(editor);
+ if (doc != null) {
+ Element element = DomUtilities.findCorresponding(equivalentElement, doc);
+ if (element != null) {
+ AdtUtils.setToolsAttribute(editor, element, undoLabel, name,
+ value, false /*reveal*/, false /*append*/);
+ if (part instanceof GraphicalEditorPart) {
+ GraphicalEditorPart g = (GraphicalEditorPart) part;
+ g.recomputeLayout();
+ g.getCanvasControl().redraw();
+ }
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean setPropertyInFile(
+ @NonNull String undoLabel,
+ @NonNull IFile variation,
+ @NonNull final Element element,
+ @NonNull final String name,
+ @Nullable final String value) {
+ Document doc = DomUtilities.getDocument(variation);
+ if (doc != null && element.getOwnerDocument() != doc) {
+ Element other = DomUtilities.findCorresponding(element, doc);
+ if (other != null) {
+ AdtUtils.setToolsAttribute(variation, other, undoLabel,
+ name, value, false);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Strips out @layout/ or @android:layout/ from the given layout reference */
+ private static String stripLayoutPrefix(String layout) {
+ if (layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
+ layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+
+ return layout;
+ }
+
+ /**
+ * Creates an {@link AdapterBinding} for the given view object, or null if the user
+ * has not yet chosen a target layout to use for the given AdapterView.
+ *
+ * @param viewObject the view object to create an adapter binding for
+ * @param map a map containing tools attribute metadata
+ * @return a binding, or null
+ */
+ @Nullable
+ public static AdapterBinding getNodeBinding(
+ @Nullable Object viewObject,
+ @NonNull Map<String, String> map) {
+ String header = map.get(KEY_LV_HEADER);
+ String footer = map.get(KEY_LV_FOOTER);
+ String layout = map.get(KEY_LV_ITEM);
+ if (layout != null || header != null || footer != null) {
+ int count = 12;
+ return getNodeBinding(viewObject, header, footer, layout, count);
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an {@link AdapterBinding} for the given view object, or null if the user
+ * has not yet chosen a target layout to use for the given AdapterView.
+ *
+ * @param viewObject the view object to create an adapter binding for
+ * @param uiNode the ui node corresponding to the view object
+ * @return a binding, or null
+ */
+ @Nullable
+ public static AdapterBinding getNodeBinding(
+ @Nullable Object viewObject,
+ @NonNull UiViewElementNode uiNode) {
+ Node xmlNode = uiNode.getXmlNode();
+
+ String header = getProperty(xmlNode, KEY_LV_HEADER);
+ String footer = getProperty(xmlNode, KEY_LV_FOOTER);
+ String layout = getProperty(xmlNode, KEY_LV_ITEM);
+ if (layout != null || header != null || footer != null) {
+ int count = 12;
+ // If we're dealing with a grid view, multiply the list item count
+ // by the number of columns to ensure we have enough items
+ if (xmlNode instanceof Element && xmlNode.getNodeName().endsWith(GRID_VIEW)) {
+ Element element = (Element) xmlNode;
+ String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS);
+ int multiplier = 2;
+ if (columns != null && columns.length() > 0 &&
+ !columns.equals(VALUE_AUTO_FIT)) {
+ try {
+ int c = Integer.parseInt(columns);
+ if (c >= 1 && c <= 10) {
+ multiplier = c;
+ }
+ } catch (NumberFormatException nufe) {
+ // some unexpected numColumns value: just stick with 2 columns for
+ // preview purposes
+ }
+ }
+ count *= multiplier;
+ }
+
+ return getNodeBinding(viewObject, header, footer, layout, count);
+ }
+
+ return null;
+ }
+
+ private static AdapterBinding getNodeBinding(Object viewObject,
+ String header, String footer, String layout, int count) {
+ if (layout != null || header != null || footer != null) {
+ AdapterBinding binding = new AdapterBinding(count);
+
+ if (header != null) {
+ boolean isFramework = header.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
+ binding.addHeader(new ResourceReference(stripLayoutPrefix(header),
+ isFramework));
+ }
+
+ if (footer != null) {
+ boolean isFramework = footer.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
+ binding.addFooter(new ResourceReference(stripLayoutPrefix(footer),
+ isFramework));
+ }
+
+ if (layout != null) {
+ boolean isFramework = layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
+ if (isFramework) {
+ layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+
+ binding.addItem(new DataBindingItem(layout, isFramework, 1));
+ } else if (viewObject != null) {
+ String listFqcn = ProjectCallback.getListAdapterViewFqcn(viewObject.getClass());
+ if (listFqcn != null) {
+ if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
+ binding.addItem(
+ new DataBindingItem(DEFAULT_EXPANDABLE_LIST_ITEM,
+ true /* isFramework */, 1));
+ } else {
+ binding.addItem(
+ new DataBindingItem(DEFAULT_LIST_ITEM,
+ true /* isFramework */, 1));
+ }
+ }
+ } else {
+ binding.addItem(
+ new DataBindingItem(DEFAULT_LIST_ITEM,
+ true /* isFramework */, 1));
+ }
+ return binding;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java
new file mode 100644
index 000000000..818b2c4ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.Point;
+
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+
+/**
+ * A {@link LayoutPoint} is a coordinate in the Android canvas (in other words,
+ * it may differ from the canvas control mouse coordinate because the canvas may
+ * be zoomed and scrolled.)
+ */
+public final class LayoutPoint {
+ /** Containing canvas which the point is relative to. */
+ private final LayoutCanvas mCanvas;
+
+ /** The X coordinate of the canvas coordinate. */
+ public final int x;
+
+ /** The Y coordinate of the canvas coordinate. */
+ public final int y;
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given event. The event
+ * must be from a {@link MouseListener} associated with the
+ * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+ * {@link MouseEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link LayoutPoint}
+ * from.
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * {@link MouseEvent}.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, MouseEvent event) {
+ // The mouse event coordinates should already be relative to the canvas
+ // widget.
+ assert event.widget == canvas : event.widget;
+ return ControlPoint.create(canvas, event).toLayout();
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given event. The event
+ * must be from a {@link DragSourceListener} associated with the
+ * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+ * {@link DragSourceEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link LayoutPoint}
+ * from.
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * {@link DragSourceEvent}.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+ // The drag source event coordinates should already be relative to the
+ // canvas widget.
+ return ControlPoint.create(canvas, event).toLayout();
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given x,y coordinates.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param x The mouse event x coordinate relative to the canvas
+ * @param y The mouse event x coordinate relative to the canvas
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * layout coordinates.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, int x, int y) {
+ return new LayoutPoint(canvas, x, y);
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} with the given X and Y coordinates.
+ *
+ * @param canvas The canvas which contains this coordinate
+ * @param x The canvas X coordinate
+ * @param y The canvas Y coordinate
+ */
+ private LayoutPoint(LayoutCanvas canvas, int x, int y) {
+ mCanvas = canvas;
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Returns the equivalent {@link ControlPoint} to this
+ * {@link LayoutPoint}.
+ *
+ * @return The equivalent {@link ControlPoint} to this
+ * {@link LayoutPoint}
+ */
+ public ControlPoint toControl() {
+ int cx = mCanvas.getHorizontalTransform().translate(x);
+ int cy = mCanvas.getVerticalTransform().translate(y);
+
+ return ControlPoint.create(mCanvas, cx, cy);
+ }
+
+ /**
+ * Returns this {@link LayoutPoint} as a {@link Point}, in the same coordinate space.
+ *
+ * @return a new {@link Point} in the same coordinate space
+ */
+ public Point toPoint() {
+ return new Point(x, y);
+ }
+
+ @Override
+ public String toString() {
+ return "LayoutPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ LayoutPoint other = (LayoutPoint) obj;
+ if (x != other.x)
+ return false;
+ if (y != other.y)
+ return false;
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java
new file mode 100644
index 000000000..56b86aa85
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.google.common.collect.Maps;
+
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IPartListener2;
+import org.eclipse.ui.IPartService;
+import org.eclipse.ui.IViewReference;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartReference;
+import org.eclipse.ui.IWorkbenchWindow;
+
+import java.util.Map;
+
+/**
+ * The {@link LayoutWindowCoordinator} keeps track of Eclipse window events (opening, closing,
+ * fronting, etc) and uses this information to manage the propertysheet and outline
+ * views such that they are always(*) showing:
+ * <ul>
+ * <li> If the Property Sheet and Outline Eclipse views are showing, it does nothing.
+ * "Showing" means "is open", not necessary "is visible", e.g. in a tabbed view
+ * there could be a different view on top.
+ * <li> If just the outline is showing, then the property sheet is shown in a sashed
+ * pane below or to the right of the outline (depending on the dominant dimension
+ * of the window).
+ * <li> TBD: If just the property sheet is showing, should the outline be showed
+ * inside that window? Not yet done.
+ * <li> If the outline is *not* showing, then the outline is instead shown
+ * <b>inside</b> the editor area, in a right-docked view! This right docked view
+ * also includes the property sheet!
+ * <li> If the property sheet is not showing (which includes not showing in the outline
+ * view as well), then it will be shown inside the editor area, along with the outline
+ * which should also be there (since if the outline was showing outside the editor
+ * area, the property sheet would have docked there).
+ * <li> When the editor is maximized, then all views are temporarily hidden. In this
+ * case, the property sheet and outline will show up inside the editor.
+ * When the editor view is un-maximized, the view state will return to what it
+ * was before.
+ * </ul>
+ * </p>
+ * There is one coordinator per workbench window, shared between all editors in that window.
+ * <p>
+ * TODO: Rename this class to AdtWindowCoordinator. It is used for more than just layout
+ * window coordination now. For example, it's also used to dispatch {@code activated()} and
+ * {@code deactivated()} events to all the XML editors, to ensure that key bindings are
+ * properly dispatched to the right editors in Eclipse 4.x.
+ */
+public class LayoutWindowCoordinator implements IPartListener2 {
+ static final String PROPERTY_SHEET_PART_ID = "org.eclipse.ui.views.PropertySheet"; //$NON-NLS-1$
+ static final String OUTLINE_PART_ID = "org.eclipse.ui.views.ContentOutline"; //$NON-NLS-1$
+ /** The workbench window */
+ private final IWorkbenchWindow mWindow;
+ /** Is the Eclipse property sheet ViewPart open? */
+ private boolean mPropertiesOpen;
+ /** Is the Eclipse outline ViewPart open? */
+ private boolean mOutlineOpen;
+ /** Is the editor maximized? */
+ private boolean mEditorMaximized;
+ /**
+ * Has the coordinator been initialized? We may have to delay initialization
+ * and perform it lazily if the workbench window does not have an active
+ * page when the coordinator is first started
+ */
+ private boolean mInitialized;
+
+ /** Map from workbench windows to each layout window coordinator instance for that window */
+ private static Map<IWorkbenchWindow, LayoutWindowCoordinator> sCoordinators =
+ Maps.newHashMapWithExpectedSize(2);
+
+ /**
+ * Returns the coordinator for the given window.
+ *
+ * @param window the associated window
+ * @param create whether to create the window if it does not already exist
+ * @return the new coordinator, never null if {@code create} is true
+ */
+ @Nullable
+ public static LayoutWindowCoordinator get(@NonNull IWorkbenchWindow window, boolean create) {
+ synchronized (LayoutWindowCoordinator.class){
+ LayoutWindowCoordinator coordinator = sCoordinators.get(window);
+ if (coordinator == null && create) {
+ coordinator = new LayoutWindowCoordinator(window);
+
+ IPartService service = window.getPartService();
+ if (service != null) {
+ // What if the editor part is *already* open? How do I deal with that?
+ service.addPartListener(coordinator);
+ }
+
+ sCoordinators.put(window, coordinator);
+ }
+
+ return coordinator;
+ }
+ }
+
+
+ /** Disposes this coordinator (when a window is closed) */
+ public void dispose() {
+ IPartService service = mWindow.getPartService();
+ if (service != null) {
+ service.removePartListener(this);
+ }
+
+ synchronized (LayoutWindowCoordinator.class){
+ sCoordinators.remove(mWindow);
+ }
+ }
+
+ /**
+ * Returns true if the main editor window is maximized
+ *
+ * @return true if the main editor window is maximized
+ */
+ public boolean isEditorMaximized() {
+ return mEditorMaximized;
+ }
+
+ private LayoutWindowCoordinator(@NonNull IWorkbenchWindow window) {
+ mWindow = window;
+
+ initialize();
+ }
+
+ private void initialize() {
+ if (mInitialized) {
+ return;
+ }
+
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage == null) {
+ return;
+ }
+
+ mInitialized = true;
+
+ // Look up current state of the properties and outline windows (in case
+ // they have already been opened before we added our part listener)
+ IViewReference ref = findPropertySheetView(activePage);
+ if (ref != null) {
+ IWorkbenchPart part = ref.getPart(false /*restore*/);
+ if (activePage.isPartVisible(part)) {
+ mPropertiesOpen = true;
+ }
+ }
+ ref = findOutlineView(activePage);
+ if (ref != null) {
+ IWorkbenchPart part = ref.getPart(false /*restore*/);
+ if (activePage.isPartVisible(part)) {
+ mOutlineOpen = true;
+ }
+ }
+ if (!syncMaximizedState(activePage)) {
+ syncActive();
+ }
+ }
+
+ static IViewReference findPropertySheetView(IWorkbenchPage activePage) {
+ return activePage.findViewReference(PROPERTY_SHEET_PART_ID);
+ }
+
+ static IViewReference findOutlineView(IWorkbenchPage activePage) {
+ return activePage.findViewReference(OUTLINE_PART_ID);
+ }
+
+ /**
+ * Checks the maximized state of the page and updates internal state if
+ * necessary.
+ * <p>
+ * This is used in Eclipse 4.x, where the {@link IPartListener2} does not
+ * fire {@link IPartListener2#partHidden(IWorkbenchPartReference)} when the
+ * editor is maximized anymore (see issue
+ * https://bugs.eclipse.org/bugs/show_bug.cgi?id=382120 for details).
+ * Instead, the layout editor listens for resize events, and upon resize it
+ * looks up the part state and calls this method to ensure that the right
+ * maximized state is known to the layout coordinator.
+ *
+ * @param page the active workbench page
+ * @return true if the state changed, false otherwise
+ */
+ public boolean syncMaximizedState(IWorkbenchPage page) {
+ boolean maximized = isPageZoomed(page);
+ if (mEditorMaximized != maximized) {
+ mEditorMaximized = maximized;
+ syncActive();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isPageZoomed(IWorkbenchPage page) {
+ IWorkbenchPartReference reference = page.getActivePartReference();
+ if (reference != null && reference instanceof IEditorReference) {
+ int state = page.getPartState(reference);
+ boolean maximized = (state & IWorkbenchPage.STATE_MAXIMIZED) != 0;
+ return maximized;
+ }
+
+ // If the active reference isn't the editor, then the editor can't be maximized
+ return false;
+ }
+
+ /**
+ * Syncs the given editor's view state such that the property sheet and or
+ * outline are shown or hidden according to the visibility of the global
+ * outline and property sheet views.
+ * <p>
+ * This is typically done when a layout editor is fronted. For view updates
+ * when the view is already showing, the {@link LayoutWindowCoordinator}
+ * will automatically handle the current fronted window.
+ *
+ * @param editor the editor to sync
+ */
+ private void sync(@Nullable GraphicalEditorPart editor) {
+ if (editor == null) {
+ return;
+ }
+ if (mEditorMaximized) {
+ editor.showStructureViews(true /*outline*/, true /*properties*/, true /*layout*/);
+ } else if (mOutlineOpen) {
+ editor.showStructureViews(false /*outline*/, false /*properties*/, true /*layout*/);
+ editor.getCanvasControl().getOutlinePage().setShowPropertySheet(!mPropertiesOpen);
+ } else {
+ editor.showStructureViews(true /*outline*/, !mPropertiesOpen /*properties*/,
+ true /*layout*/);
+ }
+ }
+
+ private void sync(IWorkbenchPart part) {
+ if (part instanceof AndroidXmlEditor) {
+ LayoutEditorDelegate editor = LayoutEditorDelegate.fromEditor((IEditorPart) part);
+ if (editor != null) {
+ sync(editor.getGraphicalEditor());
+ }
+ }
+ }
+
+ private void syncActive() {
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage != null) {
+ IEditorPart editor = activePage.getActiveEditor();
+ sync(editor);
+ }
+ }
+
+ private void propertySheetClosed() {
+ mPropertiesOpen = false;
+ syncActive();
+ }
+
+ private void propertySheetOpened() {
+ mPropertiesOpen = true;
+ syncActive();
+ }
+
+ private void outlineClosed() {
+ mOutlineOpen = false;
+ syncActive();
+ }
+
+ private void outlineOpened() {
+ mOutlineOpen = true;
+ syncActive();
+ }
+
+ // ---- Implements IPartListener2 ----
+
+ @Override
+ public void partOpened(IWorkbenchPartReference partRef) {
+ // We ignore partOpened() and partClosed() because these methods are only
+ // called when a view is opened in the first perspective, and closed in the
+ // last perspective. The outline is typically used in multiple perspectives,
+ // so closing it in the Java perspective does *not* fire a partClosed event.
+ // There is no notification for "part closed in perspective" (see issue
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=54559 for details).
+ // However, the workaround we can use is to listen to partVisible() and
+ // partHidden(). These will be called more often than we'd like (e.g.
+ // when the tab order causes a view to be obscured), however, we can use
+ // the workaround of looking up IWorkbenchPage.findViewReference(id) after
+ // partHidden(), which will return null if the view is closed in the current
+ // perspective. For partOpened, we simply look in partVisible() for whether
+ // our flags tracking the view state have been initialized already.
+ }
+
+ @Override
+ public void partClosed(IWorkbenchPartReference partRef) {
+ // partClosed() doesn't get called when a window is closed unless it has
+ // been closed in *all* perspectives. See partOpened() for more.
+ }
+
+ @Override
+ public void partHidden(IWorkbenchPartReference partRef) {
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage == null) {
+ return;
+ }
+ initialize();
+
+ // See if this looks like the window was closed in this workspace
+ // See partOpened() for an explanation.
+ String id = partRef.getId();
+ if (PROPERTY_SHEET_PART_ID.equals(id)) {
+ if (activePage.findViewReference(id) == null) {
+ propertySheetClosed();
+ return;
+ }
+ } else if (OUTLINE_PART_ID.equals(id)) {
+ if (activePage.findViewReference(id) == null) {
+ outlineClosed();
+ return;
+ }
+ }
+
+ // Does this look like a window getting maximized?
+ syncMaximizedState(activePage);
+ }
+
+ @Override
+ public void partVisible(IWorkbenchPartReference partRef) {
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage == null) {
+ return;
+ }
+ initialize();
+
+ String id = partRef.getId();
+ if (mEditorMaximized) {
+ // Return to their non-maximized state
+ mEditorMaximized = false;
+ syncActive();
+ }
+
+ IWorkbenchPart part = partRef.getPart(false /*restore*/);
+ sync(part);
+
+ // See partOpened() for an explanation
+ if (PROPERTY_SHEET_PART_ID.equals(id)) {
+ if (!mPropertiesOpen) {
+ propertySheetOpened();
+ assert mPropertiesOpen;
+ }
+ } else if (OUTLINE_PART_ID.equals(id)) {
+ if (!mOutlineOpen) {
+ outlineOpened();
+ assert mOutlineOpen;
+ }
+ }
+ }
+
+ @Override
+ public void partInputChanged(IWorkbenchPartReference partRef) {
+ }
+
+ @Override
+ public void partActivated(IWorkbenchPartReference partRef) {
+ IWorkbenchPart part = partRef.getPart(false);
+ if (part instanceof AndroidXmlEditor) {
+ ((AndroidXmlEditor)part).activated();
+ }
+ }
+
+ @Override
+ public void partBroughtToTop(IWorkbenchPartReference partRef) {
+ }
+
+ @Override
+ public void partDeactivated(IWorkbenchPartReference partRef) {
+ IWorkbenchPart part = partRef.getPart(false);
+ if (part instanceof AndroidXmlEditor) {
+ ((AndroidXmlEditor)part).deactivated();
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java
new file mode 100644
index 000000000..ca74493e8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.Collection;
+
+/**
+ * The {@link LintOverlay} paints an icon over each view that contains at least one
+ * lint error (unless the view is smaller than the icon)
+ */
+public class LintOverlay extends Overlay {
+ /** Approximate size of lint overlay icons */
+ static final int ICON_SIZE = 8;
+ /** Alpha to draw lint overlay icons with */
+ private static final int ALPHA = 192;
+
+ private final LayoutCanvas mCanvas;
+ private Image mWarningImage;
+ private Image mErrorImage;
+
+ /**
+ * Constructs a new {@link LintOverlay}
+ *
+ * @param canvas the associated canvas
+ */
+ public LintOverlay(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ @Override
+ public boolean isHiding() {
+ return super.isHiding() || !AdtPrefs.getPrefs().isLintOnSave();
+ }
+
+ @Override
+ public void paint(GC gc) {
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ Collection<Node> nodes = editor.getLintNodes();
+ if (nodes != null && !nodes.isEmpty()) {
+ // Copy list before iterating through it to avoid a concurrent list modification
+ // in case lint runs in the background while painting and updates this list
+ nodes = Lists.newArrayList(nodes);
+ ViewHierarchy hierarchy = mCanvas.getViewHierarchy();
+ Image icon = getWarningIcon();
+ ImageData imageData = icon.getImageData();
+ int iconWidth = imageData.width;
+ int iconHeight = imageData.height;
+ CanvasTransform mHScale = mCanvas.getHorizontalTransform();
+ CanvasTransform mVScale = mCanvas.getVerticalTransform();
+
+ // Right/bottom edges of the canvas image; don't paint overlays outside of
+ // that. (With for example RelativeLayouts with margins rendered on smaller
+ // screens than they are intended for this can happen.)
+ int maxX = mHScale.translate(0) + mHScale.getScaledImgSize();
+ int maxY = mVScale.translate(0) + mVScale.getScaledImgSize();
+
+ int oldAlpha = gc.getAlpha();
+ try {
+ gc.setAlpha(ALPHA);
+ for (Node node : nodes) {
+ CanvasViewInfo vi = hierarchy.findViewInfoFor(node);
+ if (vi != null) {
+ Rectangle bounds = vi.getAbsRect();
+ int x = mHScale.translate(bounds.x);
+ int y = mVScale.translate(bounds.y);
+ int w = mHScale.scale(bounds.width);
+ int h = mVScale.scale(bounds.height);
+ if (w < iconWidth || h < iconHeight) {
+ // Don't draw badges on tiny widgets (including those
+ // that aren't tiny but are zoomed out too far)
+ continue;
+ }
+
+ x += w - iconWidth;
+ y += h - iconHeight;
+
+ if (x > maxX || y > maxY) {
+ continue;
+ }
+
+ boolean isError = false;
+ IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
+ if (marker != null) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, 0);
+ isError = severity == IMarker.SEVERITY_ERROR;
+ }
+
+ icon = isError ? getErrorIcon() : getWarningIcon();
+
+ gc.drawImage(icon, x, y);
+ }
+ }
+ } finally {
+ gc.setAlpha(oldAlpha);
+ }
+ }
+ }
+
+ private Image getWarningIcon() {
+ if (mWarningImage == null) {
+ mWarningImage = IconFactory.getInstance().getIcon("warning-badge"); //$NON-NLS-1$
+ }
+
+ return mWarningImage;
+ }
+
+ private Image getErrorIcon() {
+ if (mErrorImage == null) {
+ mErrorImage = IconFactory.getInstance().getIcon("error-badge"); //$NON-NLS-1$
+ }
+
+ return mErrorImage;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java
new file mode 100644
index 000000000..cedd43659
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ATTR_ID;
+
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.List;
+
+/** Actual tooltip showing multiple lines for various widgets that have lint errors */
+class LintTooltip extends Shell {
+ private final LayoutCanvas mCanvas;
+ private final List<UiViewElementNode> mNodes;
+
+ LintTooltip(LayoutCanvas canvas, List<UiViewElementNode> nodes) {
+ super(canvas.getDisplay(), SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
+ mCanvas = canvas;
+ mNodes = nodes;
+
+ createContents();
+ }
+
+ protected void createContents() {
+ Display display = getDisplay();
+ Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
+ Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
+ setBackground(bg);
+ GridLayout gridLayout = new GridLayout(2, false);
+ setLayout(gridLayout);
+
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+
+ boolean first = true;
+ for (UiViewElementNode node : mNodes) {
+ IMarker marker = delegate.getIssueForNode(node);
+ if (marker != null) {
+ String message = marker.getAttribute(IMarker.MESSAGE, null);
+ if (message != null) {
+ Label icon = new Label(this, SWT.NONE);
+ icon.setForeground(fg);
+ icon.setBackground(bg);
+ icon.setImage(node.getIcon());
+
+ Label label = new Label(this, SWT.WRAP);
+ if (first) {
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1));
+ first = false;
+ }
+
+ String id = BaseLayoutRule.stripIdPrefix(node.getAttributeValue(ATTR_ID));
+ if (id.isEmpty()) {
+ if (node.getXmlNode() != null) {
+ id = node.getXmlNode().getNodeName();
+ } else {
+ id = node.getDescriptor().getUiName();
+ }
+ }
+
+ label.setText(String.format("%1$s: %2$s", id, message));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java
new file mode 100644
index 000000000..f71935889
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LintOverlay.ICON_SIZE;
+
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** Tooltip in the layout editor showing lint errors under the cursor */
+class LintTooltipManager implements Listener {
+ private final LayoutCanvas mCanvas;
+ private Shell mTip = null;
+ private List<UiViewElementNode> mShowingNodes;
+
+ /**
+ * Sets up a custom tooltip when hovering over tree items. It currently displays the error
+ * message for the lint warning associated with each node, if any (and only if the hover
+ * is over the icon portion).
+ */
+ LintTooltipManager(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ void register() {
+ mCanvas.addListener(SWT.Dispose, this);
+ mCanvas.addListener(SWT.KeyDown, this);
+ mCanvas.addListener(SWT.MouseMove, this);
+ mCanvas.addListener(SWT.MouseHover, this);
+ }
+
+ void unregister() {
+ if (!mCanvas.isDisposed()) {
+ mCanvas.removeListener(SWT.Dispose, this);
+ mCanvas.removeListener(SWT.KeyDown, this);
+ mCanvas.removeListener(SWT.MouseMove, this);
+ mCanvas.removeListener(SWT.MouseHover, this);
+ }
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ switch(event.type) {
+ case SWT.MouseMove:
+ // See if we're still overlapping this or *other* errors; if so, keep the
+ // tip up (or update it).
+ if (mShowingNodes != null) {
+ List<UiViewElementNode> nodes = computeNodes(event);
+ if (nodes != null && !nodes.isEmpty()) {
+ if (nodes.equals(mShowingNodes)) {
+ return;
+ } else {
+ show(nodes);
+ }
+ break;
+ }
+ }
+
+ // If not, fall through and hide the tooltip
+
+ //$FALL-THROUGH$
+ case SWT.Dispose:
+ case SWT.FocusOut:
+ case SWT.KeyDown:
+ case SWT.MouseExit:
+ case SWT.MouseDown:
+ hide();
+ break;
+ case SWT.MouseHover:
+ hide();
+ show(event);
+ break;
+ }
+ }
+
+ void hide() {
+ if (mTip != null) {
+ mTip.dispose();
+ mTip = null;
+ }
+ mShowingNodes = null;
+ }
+
+ private void show(Event event) {
+ List<UiViewElementNode> nodes = computeNodes(event);
+ if (nodes != null && !nodes.isEmpty()) {
+ show(nodes);
+ }
+ }
+
+ /** Show a tooltip listing the lint errors for the given nodes */
+ private void show(List<UiViewElementNode> nodes) {
+ hide();
+
+ if (!AdtPrefs.getPrefs().isLintOnSave()) {
+ return;
+ }
+
+ mTip = new LintTooltip(mCanvas, nodes);
+ Rectangle rect = mCanvas.getBounds();
+ Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ Point pos = mCanvas.toDisplay(rect.x, rect.y + rect.height);
+ if (size.x > rect.width) {
+ size = mTip.computeSize(rect.width, SWT.DEFAULT);
+ }
+ mTip.setBounds(pos.x, pos.y, size.x, size.y);
+
+ mShowingNodes = nodes;
+ mTip.setVisible(true);
+ }
+
+ /**
+ * Compute the list of nodes which have lint warnings near the given mouse
+ * coordinates
+ *
+ * @param event the mouse cursor event
+ * @return a list of nodes, possibly empty
+ */
+ @Nullable
+ private List<UiViewElementNode> computeNodes(Event event) {
+ LayoutPoint p = ControlPoint.create(mCanvas, event.x, event.y).toLayout();
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ CanvasTransform mHScale = mCanvas.getHorizontalTransform();
+ CanvasTransform mVScale = mCanvas.getVerticalTransform();
+
+ int layoutIconSize = mHScale.inverseScale(ICON_SIZE);
+ int slop = mVScale.inverseScale(10); // extra space around icon where tip triggers
+
+ Collection<Node> xmlNodes = delegate.getLintNodes();
+ if (xmlNodes == null) {
+ return null;
+ }
+ List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>();
+ for (Node xmlNode : xmlNodes) {
+ CanvasViewInfo v = viewHierarchy.findViewInfoFor(xmlNode);
+ if (v != null) {
+ Rectangle b = v.getAbsRect();
+ int x2 = b.x + b.width;
+ int y2 = b.y + b.height;
+ if (p.x < x2 - layoutIconSize - slop
+ || p.x > x2 + slop
+ || p.y < y2 - layoutIconSize - slop
+ || p.y > y2 + slop) {
+ continue;
+ }
+
+ nodes.add(v.getUiViewNode());
+ }
+ }
+
+ return nodes;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java
new file mode 100644
index 000000000..4577f8d12
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_LV_FOOTER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_LV_HEADER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_LV_ITEM;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Menu;
+import org.w3c.dom.Node;
+
+/**
+ * "Preview List Content" context menu which lists available data types and layouts
+ * the user can choose to view the ListView as.
+ */
+public class ListViewTypeMenu extends SubmenuAction {
+ /** Associated canvas */
+ private final LayoutCanvas mCanvas;
+ /** When true, this menu is for a grid rather than a simple list */
+ private boolean mGrid;
+ /** When true, this menu is for a spinner rather than a simple list */
+ private boolean mSpinner;
+
+ /**
+ * Creates a "Preview List Content" menu
+ *
+ * @param canvas associated canvas
+ * @param isGrid whether the menu is for a grid rather than a list
+ * @param isSpinner whether the menu is for a spinner rather than a list
+ */
+ public ListViewTypeMenu(LayoutCanvas canvas, boolean isGrid, boolean isSpinner) {
+ super(isGrid ? "Preview Grid Content" : isSpinner ? "Preview Spinner Layout"
+ : "Preview List Content");
+ mCanvas = canvas;
+ mGrid = isGrid;
+ mSpinner = isSpinner;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (graphicalEditor.renderingSupports(Capability.ADAPTER_BINDING)) {
+ IAction action = new PickLayoutAction("Choose Layout...", KEY_LV_ITEM);
+ new ActionContributionItem(action).fill(menu, -1);
+ new Separator().fill(menu, -1);
+
+ String selected = getSelectedLayout();
+ if (selected != null) {
+ if (selected.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
+ selected = selected.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ }
+
+ if (mSpinner) {
+ action = new SetListTypeAction("Spinner Item",
+ "simple_spinner_item", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Spinner Dropdown Item",
+ "simple_spinner_dropdown_item", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ return;
+ }
+
+ action = new SetListTypeAction("Simple List Item",
+ "simple_list_item_1", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Simple 2-Line List Item",
+ "simple_list_item_2", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Checked List Item",
+ "simple_list_item_checked", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Single Choice List Item",
+ "simple_list_item_single_choice", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Multiple Choice List Item",
+ "simple_list_item_multiple_choice", //$NON-NLS-1$
+ selected);
+ if (!mGrid) {
+ new Separator().fill(menu, -1);
+ action = new SetListTypeAction("Simple Expandable List Item",
+ "simple_expandable_list_item_1", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Simple 2-Line Expandable List Item",
+ "simple_expandable_list_item_2", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+
+ new Separator().fill(menu, -1);
+ action = new PickLayoutAction("Choose Header...", KEY_LV_HEADER);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new PickLayoutAction("Choose Footer...", KEY_LV_FOOTER);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ } else {
+ // Should we just hide the menu item instead?
+ addDisabledMessageItem(
+ "Not supported for this SDK version; try changing the Render Target");
+ }
+ }
+
+ private class SetListTypeAction extends Action {
+ private final String mLayout;
+
+ public SetListTypeAction(String title, String layout, String selected) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mLayout = layout;
+
+ if (layout.equals(selected)) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (isChecked()) {
+ setNewType(KEY_LV_ITEM, ANDROID_LAYOUT_RESOURCE_PREFIX + mLayout);
+ }
+ }
+ }
+
+ /**
+ * Action which brings up a resource chooser to choose an arbitrary layout as the
+ * layout to be previewed in the list.
+ */
+ private class PickLayoutAction extends Action {
+ private final String mType;
+
+ public PickLayoutAction(String title, String type) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mType = type;
+ }
+
+ @Override
+ public void run() {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ IFile file = delegate.getEditor().getInputFile();
+ GraphicalEditorPart editor = delegate.getGraphicalEditor();
+ ResourceChooser dlg = ResourceChooser.create(editor, ResourceType.LAYOUT)
+ .setInputValidator(CyclicDependencyValidator.create(file))
+ .setInitialSize(85, 10)
+ .setCurrentResource(getSelectedLayout());
+ int result = dlg.open();
+ if (result == ResourceChooser.CLEAR_RETURN_CODE) {
+ setNewType(mType, null);
+ } else if (result == Window.OK) {
+ String newType = dlg.getCurrentResource();
+ setNewType(mType, newType);
+ }
+ }
+ }
+
+ @Nullable
+ private String getSelectedLayout() {
+ String layout = null;
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ for (SelectionItem item : selectionManager.getSelections()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ Node xmlNode = node.getXmlNode();
+ layout = LayoutMetadata.getProperty(xmlNode, KEY_LV_ITEM);
+ if (layout != null) {
+ return layout;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void setNewType(@NonNull String type, @Nullable String layout) {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor();
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ for (SelectionItem item : selectionManager.getSnapshot()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ Node xmlNode = node.getXmlNode();
+ LayoutMetadata.setProperty(delegate.getEditor(), xmlNode, type, layout);
+ }
+ }
+
+ // Refresh
+ graphicalEditor.recomputeLayout();
+ mCanvas.redraw();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java
new file mode 100644
index 000000000..4cfd4fe3d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link MarqueeGesture} is a gesture for swiping out a selection rectangle.
+ * With a modifier key, items that intersect the rectangle can be toggled
+ * instead of added to the new selection set.
+ */
+public class MarqueeGesture extends Gesture {
+ /** The {@link Overlay} drawn for the marquee. */
+ private MarqueeOverlay mOverlay;
+
+ /** The canvas associated with this gesture. */
+ private LayoutCanvas mCanvas;
+
+ /** A copy of the initial selection, when we're toggling the marquee. */
+ private Collection<CanvasViewInfo> mInitialSelection;
+
+ /**
+ * Creates a new marquee selection (selection swiping).
+ *
+ * @param canvas The canvas where selection is performed.
+ * @param toggle If true, toggle the membership of contained elements
+ * instead of adding it.
+ */
+ public MarqueeGesture(LayoutCanvas canvas, boolean toggle) {
+ mCanvas = canvas;
+
+ if (toggle) {
+ List<SelectionItem> selection = canvas.getSelectionManager().getSelections();
+ mInitialSelection = new ArrayList<CanvasViewInfo>(selection.size());
+ for (SelectionItem item : selection) {
+ mInitialSelection.add(item.getViewInfo());
+ }
+ } else {
+ mInitialSelection = Collections.emptySet();
+ }
+ }
+
+ @Override
+ public void update(ControlPoint pos) {
+ if (mOverlay == null) {
+ return;
+ }
+
+ int x = Math.min(pos.x, mStart.x);
+ int y = Math.min(pos.y, mStart.y);
+ int w = Math.abs(pos.x - mStart.x);
+ int h = Math.abs(pos.y - mStart.y);
+
+ mOverlay.updateSize(x, y, w, h);
+
+ // Compute selection overlaps
+ LayoutPoint topLeft = ControlPoint.create(mCanvas, x, y).toLayout();
+ LayoutPoint bottomRight = ControlPoint.create(mCanvas, x + w, y + h).toLayout();
+ mCanvas.getSelectionManager().selectWithin(topLeft, bottomRight, mInitialSelection);
+ }
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new MarqueeOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ /**
+ * An {@link Overlay} for the {@link MarqueeGesture}; paints a selection
+ * overlay rectangle matching the mouse coordinate delta between gesture
+ * start and the current position.
+ */
+ private static class MarqueeOverlay extends Overlay {
+ /** Rectangle border color. */
+ private Color mStroke;
+
+ /** Rectangle fill color. */
+ private Color mFill;
+
+ /** Current rectangle coordinates (in terms of control coordinates). */
+ private Rectangle mRectangle = new Rectangle(0, 0, 0, 0);
+
+ /** Alpha value of the fill. */
+ private int mFillAlpha;
+
+ /** Alpha value of the border. */
+ private int mStrokeAlpha;
+
+ /** Constructs a new {@link MarqueeOverlay}. */
+ public MarqueeOverlay() {
+ }
+
+ /**
+ * Updates the size of the marquee rectangle.
+ *
+ * @param x The top left corner of the rectangle, x coordinate.
+ * @param y The top left corner of the rectangle, y coordinate.
+ * @param w Rectangle width.
+ * @param h Rectangle height.
+ */
+ public void updateSize(int x, int y, int w, int h) {
+ mRectangle.x = x;
+ mRectangle.y = y;
+ mRectangle.width = w;
+ mRectangle.height = h;
+ }
+
+ @Override
+ public void create(Device device) {
+ // TODO: Integrate DrawingStyles with this?
+ mStroke = new Color(device, 255, 255, 255);
+ mFill = new Color(device, 128, 128, 128);
+ mFillAlpha = 64;
+ mStrokeAlpha = 255;
+ }
+
+ @Override
+ public void dispose() {
+ mStroke.dispose();
+ mFill.dispose();
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mRectangle.width > 0 && mRectangle.height > 0) {
+ gc.setLineStyle(SWT.LINE_SOLID);
+ gc.setLineWidth(1);
+ gc.setForeground(mStroke);
+ gc.setBackground(mFill);
+ gc.setAlpha(mStrokeAlpha);
+ gc.drawRectangle(mRectangle);
+ gc.setAlpha(mFillAlpha);
+ gc.fillRectangle(mRectangle);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java
new file mode 100644
index 000000000..7cf3a647a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java
@@ -0,0 +1,852 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.NodeCreationListener;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The Move gesture provides the operation for moving widgets around in the canvas.
+ */
+public class MoveGesture extends DropGesture {
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /** Overlay which paints the drag &amp; drop feedback. */
+ private MoveOverlay mOverlay;
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * The top view right under the drag'n'drop cursor.
+ * This can only be null during a drag'n'drop when there is no view under the cursor
+ * or after the state was all cleared.
+ */
+ private CanvasViewInfo mCurrentView;
+
+ /**
+ * The elements currently being dragged. This will always be non-null for a valid
+ * drag'n'drop that happens within the same instance of Eclipse.
+ * <p/>
+ * In the event that the drag and drop happens between different instances of Eclipse
+ * this will remain null.
+ */
+ private SimpleElement[] mCurrentDragElements;
+
+ /**
+ * The first view under the cursor that responded to onDropEnter is called the "target view".
+ * It can differ from mCurrentView, typically because a terminal View doesn't
+ * accept drag'n'drop so its parent layout became the target drag'n'drop receiver.
+ * <p/>
+ * The target node is the proxy node associated with the target view.
+ * This can be null if no view under the cursor accepted the drag'n'drop or if the node
+ * factory couldn't create a proxy for it.
+ */
+ private NodeProxy mTargetNode;
+
+ /**
+ * The latest drop feedback returned by IViewRule.onDropEnter/Move.
+ */
+ private DropFeedback mFeedback;
+
+ /**
+ * {@link #dragLeave(DropTargetEvent)} is unfortunately called right before data is
+ * about to be dropped (between the last {@link #dragOver(DropTargetEvent)} and the
+ * next {@link #dropAccept(DropTargetEvent)}). That means we can't just
+ * trash the current DropFeedback from the current view rule in dragLeave().
+ * Instead we preserve it in mLeaveTargetNode and mLeaveFeedback in case a dropAccept
+ * happens next.
+ */
+ private NodeProxy mLeaveTargetNode;
+
+ /**
+ * @see #mLeaveTargetNode
+ */
+ private DropFeedback mLeaveFeedback;
+
+ /**
+ * @see #mLeaveTargetNode
+ */
+ private CanvasViewInfo mLeaveView;
+
+ /** Singleton used to keep track of drag selection in the same Eclipse instance. */
+ private final GlobalCanvasDragInfo mGlobalDragInfo;
+
+ /**
+ * Constructs a new {@link MoveGesture}, tied to the given canvas.
+ *
+ * @param canvas The canvas to associate the {@link MoveGesture} with.
+ */
+ public MoveGesture(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
+ }
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new MoveOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ @Override
+ public void begin(ControlPoint pos, int startMask) {
+ super.begin(pos, startMask);
+
+ // Hide selection overlays during a move drag
+ mCanvas.getSelectionOverlay().setHidden(true);
+ }
+
+ @Override
+ public void end(ControlPoint pos, boolean canceled) {
+ super.end(pos, canceled);
+
+ mCanvas.getSelectionOverlay().setHidden(false);
+
+ // Ensure that the outline is back to showing the current selection, since during
+ // a drag gesture we temporarily set it to show the current target node instead.
+ mCanvas.getSelectionManager().syncOutlineSelection();
+ }
+
+ /* TODO: Pass modifier mask to drag rules as well! This doesn't work yet since
+ the drag &amp; drop code seems to steal keyboard events.
+ @Override
+ public boolean keyPressed(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+
+ @Override
+ public boolean keyReleased(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+ */
+
+ /*
+ * The cursor has entered the drop target boundaries.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragEnter(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event);
+
+ // Make sure we don't have any residual data from an earlier operation.
+ clearDropInfo();
+ mLeaveTargetNode = null;
+ mLeaveFeedback = null;
+ mLeaveView = null;
+
+ // Get the dragged elements.
+ //
+ // The current transfered type can be extracted from the event.
+ // As described in dragOver(), this works basically works on Windows but
+ // not on Linux or Mac, in which case we can't get the type until we
+ // receive dropAccept/drop().
+ // For consistency we try to use the GlobalCanvasDragInfo instance first,
+ // and if it fails we use the event transfer type as a backup (but as said
+ // before it will most likely work only on Windows.)
+ // In any case this can be null even for a valid transfer.
+
+ mCurrentDragElements = mGlobalDragInfo.getCurrentElements();
+
+ if (mCurrentDragElements == null) {
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ if (sxt.isSupportedType(event.currentDataType)) {
+ mCurrentDragElements = (SimpleElement[]) sxt.nativeToJava(event.currentDataType);
+ }
+ }
+
+ // if there is no data to transfer, invalidate the drag'n'drop.
+ // The assumption is that the transfer should have at least one element with a
+ // a non-null non-empty FQCN. Everything else is optional.
+ if (mCurrentDragElements == null ||
+ mCurrentDragElements.length == 0 ||
+ mCurrentDragElements[0] == null ||
+ mCurrentDragElements[0].getFqcn() == null ||
+ mCurrentDragElements[0].getFqcn().length() == 0) {
+ event.detail = DND.DROP_NONE;
+ }
+
+ dragOperationChanged(event);
+ }
+
+ /*
+ * The operation being performed has changed (e.g. modifier key).
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragOperationChanged(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);
+
+ checkDataType(event);
+ recomputeDragType(event);
+ }
+
+ private void recomputeDragType(DropTargetEvent event) {
+ if (event.detail == DND.DROP_DEFAULT) {
+ // Default means we can now choose the default operation, either copy or move.
+ // If the drag comes from the same canvas we default to move, otherwise we
+ // default to copy.
+
+ if (mGlobalDragInfo.getSourceCanvas() == mCanvas &&
+ (event.operations & DND.DROP_MOVE) != 0) {
+ event.detail = DND.DROP_MOVE;
+ } else if ((event.operations & DND.DROP_COPY) != 0) {
+ event.detail = DND.DROP_COPY;
+ }
+ }
+
+ // We don't support other types than copy and move
+ if (event.detail != DND.DROP_COPY && event.detail != DND.DROP_MOVE) {
+ event.detail = DND.DROP_NONE;
+ }
+ }
+
+ /*
+ * The cursor has left the drop target boundaries OR data is about to be dropped.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragLeave(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag leave");
+
+ // dragLeave is unfortunately called right before data is about to be dropped
+ // (between the last dropMove and the next dropAccept). That means we can't just
+ // trash the current DropFeedback from the current view rule, we need to preserve
+ // it in case a dropAccept happens next.
+ // See the corresponding kludge in dropAccept().
+ mLeaveTargetNode = mTargetNode;
+ mLeaveFeedback = mFeedback;
+ mLeaveView = mCurrentView;
+
+ clearDropInfo();
+ }
+
+ /*
+ * The cursor is moving over the drop target.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragOver(DropTargetEvent event) {
+ processDropEvent(event);
+ }
+
+ /*
+ * The drop is about to be performed.
+ * The drop target is given a last chance to change the nature of the drop.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dropAccept(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop accept");
+
+ checkDataType(event);
+
+ // If we have a valid target node and it matches the one we saved in
+ // dragLeave then we restore the DropFeedback that we saved in dragLeave.
+ if (mLeaveTargetNode != null) {
+ mTargetNode = mLeaveTargetNode;
+ mFeedback = mLeaveFeedback;
+ mCurrentView = mLeaveView;
+ }
+
+ if (mFeedback != null && mFeedback.invalidTarget) {
+ // The script said we can't drop here.
+ event.detail = DND.DROP_NONE;
+ }
+
+ if (mLeaveTargetNode == null || event.detail == DND.DROP_NONE) {
+ clearDropInfo();
+ }
+
+ mLeaveTargetNode = null;
+ mLeaveFeedback = null;
+ mLeaveView = null;
+ }
+
+ /*
+ * The data is being dropped.
+ * {@inheritDoc}
+ */
+ @Override
+ public void drop(final DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped");
+
+ SimpleElement[] elements = null;
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+
+ if (sxt.isSupportedType(event.currentDataType)) {
+ if (event.data instanceof SimpleElement[]) {
+ elements = (SimpleElement[]) event.data;
+ }
+ }
+
+ if (elements == null || elements.length < 1) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop missing drop data");
+ return;
+ }
+
+ if (mCurrentDragElements != null && Arrays.equals(elements, mCurrentDragElements)) {
+ elements = mCurrentDragElements;
+ }
+
+ if (mTargetNode == null) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ if (viewHierarchy.isValid() && viewHierarchy.isEmpty()) {
+ // There is no target node because the drop happens on an empty document.
+ // Attempt to create a root node accordingly.
+ createDocumentRoot(elements);
+ } else {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped on null targetNode");
+ }
+ return;
+ }
+
+ updateDropFeedback(mFeedback, event);
+
+ final SimpleElement[] elementsFinal = elements;
+ final LayoutPoint canvasPoint = getDropLocation(event).toLayout();
+ String label = computeUndoLabel(mTargetNode, elements, event.detail);
+
+ // Create node listener which (during the drop) listens for node additions
+ // and stores the list of added node such that they can be selected afterwards.
+ final List<UiElementNode> added = new ArrayList<UiElementNode>();
+ // List of "index within parent" for each node
+ final List<Integer> indices = new ArrayList<Integer>();
+ NodeCreationListener listener = new NodeCreationListener() {
+ @Override
+ public void nodeCreated(UiElementNode parent, UiElementNode child, int index) {
+ if (parent == mTargetNode.getNode()) {
+ added.add(child);
+
+ // Adjust existing indices
+ for (int i = 0, n = indices.size(); i < n; i++) {
+ int idx = indices.get(i);
+ if (idx >= index) {
+ indices.set(i, idx + 1);
+ }
+ }
+
+ indices.add(index);
+ }
+ }
+
+ @Override
+ public void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex) {
+ if (parent == mTargetNode.getNode()) {
+ // Adjust existing indices
+ for (int i = 0, n = indices.size(); i < n; i++) {
+ int idx = indices.get(i);
+ if (idx >= previousIndex) {
+ indices.set(i, idx - 1);
+ }
+ }
+
+ // Make sure we aren't removing the same nodes that are being added
+ // No, that can happen when canceling out of a drop handler such as
+ // when dropping an included layout, then canceling out of the
+ // resource chooser.
+ //assert !added.contains(child);
+ }
+ }
+ };
+
+ try {
+ UiElementNode.addNodeCreationListener(listener);
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ InsertType insertType = getInsertType(event, mTargetNode);
+ mCanvas.getRulesEngine().callOnDropped(mTargetNode,
+ elementsFinal,
+ mFeedback,
+ new Point(canvasPoint.x, canvasPoint.y),
+ insertType);
+ mTargetNode.applyPendingChanges();
+ // Clean up drag if applicable
+ if (event.detail == DND.DROP_MOVE) {
+ GlobalCanvasDragInfo.getInstance().removeSource();
+ }
+ mTargetNode.applyPendingChanges();
+ }
+ });
+ } finally {
+ UiElementNode.removeNodeCreationListener(listener);
+ }
+
+ final List<INode> nodes = new ArrayList<INode>();
+ NodeFactory nodeFactory = mCanvas.getNodeFactory();
+ for (UiElementNode uiNode : added) {
+ if (uiNode instanceof UiViewElementNode) {
+ NodeProxy node = nodeFactory.create((UiViewElementNode) uiNode);
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ }
+
+ // Select the newly dropped nodes:
+ // Find out which nodes were added, and look up their corresponding
+ // CanvasViewInfos.
+ final SelectionManager selectionManager = mCanvas.getSelectionManager();
+ // Don't use the indices to search for corresponding nodes yet, since a
+ // render may not have happened yet and we'd rather use an up to date
+ // view hierarchy than indices to look up the right view infos.
+ if (!selectionManager.selectDropped(nodes, null /* indices */)) {
+ // In some scenarios we can't find the actual view infos yet; this
+ // seems to happen when you drag from one canvas to another (see the
+ // related comment next to the setFocus() call below). In that case
+ // defer selection briefly until the view hierarchy etc is up to
+ // date.
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ selectionManager.selectDropped(nodes, indices);
+ }
+ });
+ }
+
+ clearDropInfo();
+ mCanvas.redraw();
+ // Request focus: This is *necessary* when you are dragging from one canvas editor
+ // to another, because without it, the redraw does not seem to be processed (the change
+ // is invisible until you click on the target canvas to give it focus).
+ mCanvas.setFocus();
+ }
+
+ /**
+ * Returns the right {@link InsertType} to use for the given drop target event and the
+ * given target node
+ *
+ * @param event the drop target event
+ * @param mTargetNode the node targeted by the drop
+ * @return the {link InsertType} to use for the drop
+ */
+ public static InsertType getInsertType(DropTargetEvent event, NodeProxy mTargetNode) {
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ if (event.detail == DND.DROP_MOVE) {
+ SelectionItem[] selection = dragInfo.getCurrentSelection();
+ if (selection != null) {
+ for (SelectionItem item : selection) {
+ if (item.getNode() != null
+ && item.getNode().getParent() == mTargetNode) {
+ return InsertType.MOVE_WITHIN;
+ }
+ }
+ }
+
+ return InsertType.MOVE_INTO;
+ } else if (dragInfo.getSourceCanvas() != null) {
+ return InsertType.PASTE;
+ } else {
+ return InsertType.CREATE;
+ }
+ }
+
+ /**
+ * Computes a suitable Undo label to use for a drop operation, such as
+ * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout".
+ *
+ * @param targetNode The target of the drop
+ * @param elements The dragged widgets
+ * @param detail The DnD mode, as used in {@link DropTargetEvent#detail}.
+ * @return A string suitable as an undo-label for the drop event
+ */
+ public static String computeUndoLabel(NodeProxy targetNode,
+ SimpleElement[] elements, int detail) {
+ // Decide whether it's a move or a copy; we'll label moves specifically
+ // as a move and consider everything else a "Drop"
+ String verb = (detail == DND.DROP_MOVE) ? "Move" : "Drop";
+
+ // Get the type of widget being dropped/moved, IF there is only one. If
+ // there is more than one, just reference it as "Widgets".
+ String object;
+ if (elements != null && elements.length == 1) {
+ object = getSimpleName(elements[0].getFqcn());
+ } else {
+ object = "Widgets";
+ }
+
+ String where = getSimpleName(targetNode.getFqcn());
+
+ // When we localize this: $1 is the verb (Move or Drop), $2 is the
+ // object (such as "Button"), and $3 is the place we are doing it (such
+ // as "LinearLayout").
+ return String.format("%1$s %2$s in %3$s", verb, object, where);
+ }
+
+ /**
+ * Returns simple name (basename, following last dot) of a fully qualified
+ * class name.
+ *
+ * @param fqcn The fqcn to reduce
+ * @return The base name of the fqcn
+ */
+ public static String getSimpleName(String fqcn) {
+ // Note that the following works even when there is no dot, since
+ // lastIndexOf will return -1 so we get fcqn.substring(-1+1) =
+ // fcqn.substring(0) = fqcn
+ return fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ }
+
+ /**
+ * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields
+ * of the given {@link DropFeedback}. This is generally called right before invoking
+ * one of the callOnXyz methods of GRE to refresh the fields.
+ *
+ * @param df The current {@link DropFeedback}.
+ * @param event An optional event to determine if the current operation is copy or move.
+ */
+ private void updateDropFeedback(DropFeedback df, DropTargetEvent event) {
+ if (event != null) {
+ df.isCopy = event.detail == DND.DROP_COPY;
+ }
+ df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas();
+ df.invalidTarget = false;
+ df.dipScale = mCanvas.getEditorDelegate().getGraphicalEditor().getDipScale();
+ df.modifierMask = mCanvas.getGestureManager().getRuleModifierMask();
+
+ // Set the drag bounds, after converting it from control coordinates to
+ // layout coordinates
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ Rect dragBounds = null;
+ Rect controlDragBounds = dragInfo.getDragBounds();
+ if (controlDragBounds != null) {
+ CanvasTransform ht = mCanvas.getHorizontalTransform();
+ CanvasTransform vt = mCanvas.getVerticalTransform();
+ double horizScale = ht.getScale();
+ double verticalScale = vt.getScale();
+ int x = (int) (controlDragBounds.x / horizScale);
+ int y = (int) (controlDragBounds.y / verticalScale);
+ int w = (int) (controlDragBounds.w / horizScale);
+ int h = (int) (controlDragBounds.h / verticalScale);
+ dragBounds = new Rect(x, y, w, h);
+ }
+ int baseline = dragInfo.getDragBaseline();
+ if (baseline != -1) {
+ df.dragBaseline = baseline;
+ }
+ df.dragBounds = dragBounds;
+ }
+
+ /**
+ * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}.
+ * If not, try to find a valid data type.
+ * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it.
+ *
+ * @return True if the data type is accepted.
+ */
+ private static boolean checkDataType(DropTargetEvent event) {
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+
+ TransferData current = event.currentDataType;
+
+ if (sxt.isSupportedType(current)) {
+ return true;
+ }
+
+ // We only support SimpleXmlTransfer and the current data type is not right.
+ // Let's see if we can find another one.
+
+ for (TransferData td : event.dataTypes) {
+ if (td != current && sxt.isSupportedType(td)) {
+ // We like this type better.
+ event.currentDataType = td;
+ return true;
+ }
+ }
+
+ // We failed to find any good transfer type.
+ event.detail = DND.DROP_NONE;
+ return false;
+ }
+
+ /**
+ * Returns the mouse location of the drop target event.
+ *
+ * @param event the drop target event
+ * @return a {@link ControlPoint} location corresponding to the top left corner
+ */
+ private ControlPoint getDropLocation(DropTargetEvent event) {
+ return ControlPoint.create(mCanvas, event);
+ }
+
+ /**
+ * Called on both dragEnter and dragMove.
+ * Generates the onDropEnter/Move/Leave events depending on the currently
+ * selected target node.
+ */
+ private void processDropEvent(DropTargetEvent event) {
+ if (!mCanvas.getViewHierarchy().isValid()) {
+ // We don't allow drop on an invalid layout, even if we have some obsolete
+ // layout info for it.
+ event.detail = DND.DROP_NONE;
+ clearDropInfo();
+ return;
+ }
+
+ LayoutPoint p = getDropLocation(event).toLayout();
+
+ // Is the mouse currently captured by a DropFeedback.captureArea?
+ boolean isCaptured = false;
+ if (mFeedback != null) {
+ Rect r = mFeedback.captureArea;
+ isCaptured = r != null && r.contains(p.x, p.y);
+ }
+
+ // We can't switch views/nodes when the mouse is captured
+ CanvasViewInfo vi;
+ if (isCaptured) {
+ vi = mCurrentView;
+ } else {
+ vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+
+ // When dragging into the canvas, if you are not over any other view, target
+ // the root element (since it may not "fill" the screen, e.g. if you have a linear
+ // layout but have layout_height wrap_content, then the layout will only extend
+ // to cover the children in the layout, not the whole visible screen area, which
+ // may be surprising
+ if (vi == null) {
+ vi = mCanvas.getViewHierarchy().getRoot();
+ }
+ }
+
+ boolean isMove = true;
+ boolean needRedraw = false;
+
+ if (vi != mCurrentView) {
+ // Current view has changed. Does that also change the target node?
+ // Note that either mCurrentView or vi can be null.
+
+ if (vi == null) {
+ // vi is null but mCurrentView is not, no view is a target anymore
+ // We don't need onDropMove in this case
+ isMove = false;
+ needRedraw = true;
+ event.detail = DND.DROP_NONE;
+ clearDropInfo(); // this will call callDropLeave.
+
+ } else {
+ // vi is a new current view.
+ // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child
+ // towards its parent, till we find one that returns a non-null drop feedback.
+
+ DropFeedback df = null;
+ NodeProxy targetNode = null;
+
+ for (CanvasViewInfo targetVi = vi;
+ targetVi != null && df == null;
+ targetVi = targetVi.getParent()) {
+ targetNode = mCanvas.getNodeFactory().create(targetVi);
+ df = mCanvas.getRulesEngine().callOnDropEnter(targetNode,
+ targetVi.getViewObject(), mCurrentDragElements);
+
+ if (df != null) {
+ // We should also dispatch an onDropMove() call to the initial enter
+ // position, such that the view is notified of the position where
+ // we are within the node immediately (before we for example attempt
+ // to draw feedback). This is necessary since most views perform the
+ // guideline computations in onDropMove (since only onDropMove is handed
+ // the -position- of the mouse), and we want this computation to happen
+ // before we ask the view to draw its feedback.
+ updateDropFeedback(df, event);
+ df = mCanvas.getRulesEngine().callOnDropMove(targetNode,
+ mCurrentDragElements, df, new Point(p.x, p.y));
+ }
+
+ if (df != null &&
+ event.detail == DND.DROP_MOVE &&
+ mCanvas == mGlobalDragInfo.getSourceCanvas()) {
+ // You can't move an object into itself in the same canvas.
+ // E.g. case of moving a layout and the node under the mouse is the
+ // layout itself: a copy would be ok but not a move operation of the
+ // layout into himself.
+
+ SelectionItem[] selection = mGlobalDragInfo.getCurrentSelection();
+ if (selection != null) {
+ for (SelectionItem cs : selection) {
+ if (cs.getViewInfo() == targetVi) {
+ // The node that responded is one of the selection roots.
+ // Simply invalidate the drop feedback and move on the
+ // parent in the ViewInfo chain.
+
+ updateDropFeedback(df, event);
+ mCanvas.getRulesEngine().callOnDropLeave(
+ targetNode, mCurrentDragElements, df);
+ df = null;
+ targetNode = null;
+ }
+ }
+ }
+ }
+ }
+
+ if (df == null) {
+ // Provide visual feedback that we are refusing the drop
+ event.detail = DND.DROP_NONE;
+ clearDropInfo();
+
+ } else if (targetNode != mTargetNode) {
+ // We found a new target node for the drag'n'drop.
+ // Release the previous one, if any.
+ callDropLeave();
+
+ // And assign the new one
+ mTargetNode = targetNode;
+ mFeedback = df;
+
+ // We don't need onDropMove in this case
+ isMove = false;
+ }
+ }
+
+ mCurrentView = vi;
+ }
+
+ if (isMove && mTargetNode != null && mFeedback != null) {
+ // this is a move inside the same view
+ com.android.ide.common.api.Point p2 =
+ new com.android.ide.common.api.Point(p.x, p.y);
+ updateDropFeedback(mFeedback, event);
+ DropFeedback df = mCanvas.getRulesEngine().callOnDropMove(
+ mTargetNode, mCurrentDragElements, mFeedback, p2);
+ mCanvas.getGestureManager().updateMessage(mFeedback);
+
+ if (df == null) {
+ // The target is no longer interested in the drop move.
+ event.detail = DND.DROP_NONE;
+ callDropLeave();
+
+ } else if (df != mFeedback) {
+ mFeedback = df;
+ }
+ }
+
+ if (mFeedback != null) {
+ if (event.detail == DND.DROP_NONE && !mFeedback.invalidTarget) {
+ // If we previously provided visual feedback that we were refusing
+ // the drop, we now need to change it to mean we're accepting it.
+ event.detail = DND.DROP_DEFAULT;
+ recomputeDragType(event);
+
+ } else if (mFeedback.invalidTarget) {
+ // Provide visual feedback that we are refusing the drop
+ event.detail = DND.DROP_NONE;
+ }
+ }
+
+ if (needRedraw || (mFeedback != null && mFeedback.requestPaint)) {
+ mCanvas.redraw();
+ }
+
+ // Update outline to show the target node there
+ OutlinePage outline = mCanvas.getOutlinePage();
+ TreeSelection newSelection = TreeSelection.EMPTY;
+ if (mCurrentView != null && mTargetNode != null) {
+ // Find the view corresponding to the target node. The current view can be a leaf
+ // view whereas the target node is always a parent layout.
+ if (mCurrentView.getUiViewNode() != mTargetNode.getNode()) {
+ mCurrentView = mCurrentView.getParent();
+ }
+ if (mCurrentView != null && mCurrentView.getUiViewNode() == mTargetNode.getNode()) {
+ TreePath treePath = SelectionManager.getTreePath(mCurrentView);
+ newSelection = new TreeSelection(treePath);
+ }
+ }
+
+ ISelection currentSelection = outline.getSelection();
+ if (currentSelection == null || !currentSelection.equals(newSelection)) {
+ outline.setSelection(newSelection);
+ }
+ }
+
+ /**
+ * Calls onDropLeave on mTargetNode with the current mFeedback. <br/>
+ * Then clears mTargetNode and mFeedback.
+ */
+ private void callDropLeave() {
+ if (mTargetNode != null && mFeedback != null) {
+ updateDropFeedback(mFeedback, null);
+ mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback);
+ }
+
+ mTargetNode = null;
+ mFeedback = null;
+ }
+
+ private void clearDropInfo() {
+ callDropLeave();
+ mCurrentView = null;
+ mCanvas.redraw();
+ }
+
+ /**
+ * Creates a root element in an empty document.
+ * Only the first element's FQCN of the dragged elements is used.
+ * <p/>
+ * Actual XML handling is done by {@link LayoutCanvas#createDocumentRoot(String)}.
+ */
+ private void createDocumentRoot(SimpleElement[] elements) {
+ if (elements == null || elements.length < 1 || elements[0] == null) {
+ return;
+ }
+
+ mCanvas.createDocumentRoot(elements[0]);
+ }
+
+ /**
+ * An {@link Overlay} to paint the move feedback. This just delegates to the
+ * layout rules.
+ */
+ private class MoveOverlay extends Overlay {
+ @Override
+ public void paint(GC gc) {
+ if (mTargetNode != null && mFeedback != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback);
+ mFeedback.requestPaint = false;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java
new file mode 100644
index 000000000..1af3053e3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.util.ArrayList;
+
+/** Drag listener for the outline page */
+/* package */ class OutlineDragListener implements DragSourceListener {
+ private TreeViewer mTreeViewer;
+ private OutlinePage mOutlinePage;
+ private final ArrayList<SelectionItem> mDragSelection = new ArrayList<SelectionItem>();
+ private SimpleElement[] mDragElements;
+
+ public OutlineDragListener(OutlinePage outlinePage, TreeViewer treeViewer) {
+ super();
+ mOutlinePage = outlinePage;
+ mTreeViewer = treeViewer;
+ }
+
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ Tree tree = mTreeViewer.getTree();
+
+ TreeItem overTreeItem = tree.getItem(new Point(e.x, e.y));
+ if (overTreeItem == null) {
+ // Not dragging over a tree item
+ e.doit = false;
+ return;
+ }
+ CanvasViewInfo over = getViewInfo(overTreeItem);
+ if (over == null) {
+ e.doit = false;
+ return;
+ }
+
+ // The selection logic for the outline is much simpler than in the canvas,
+ // because for one thing, the tree selection is updated synchronously on mouse
+ // down, so it's not possible to start dragging a non-selected item.
+ // We also don't deliberately disallow root-element dragging since you can
+ // drag it into another form.
+ final LayoutCanvas canvas = mOutlinePage.getEditor().getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ TreeItem[] treeSelection = tree.getSelection();
+ mDragSelection.clear();
+ for (TreeItem item : treeSelection) {
+ CanvasViewInfo viewInfo = getViewInfo(item);
+ if (viewInfo != null) {
+ mDragSelection.add(selectionManager.createSelection(viewInfo));
+ }
+ }
+ SelectionManager.sanitize(mDragSelection);
+
+ e.doit = !mDragSelection.isEmpty();
+ int imageCount = mDragSelection.size();
+ if (e.doit) {
+ mDragElements = SelectionItem.getAsElements(mDragSelection);
+ GlobalCanvasDragInfo.getInstance().startDrag(mDragElements,
+ mDragSelection.toArray(new SelectionItem[imageCount]),
+ canvas, new Runnable() {
+ @Override
+ public void run() {
+ canvas.getClipboardSupport().deleteSelection("Remove",
+ mDragSelection);
+ }
+ });
+ return;
+ }
+
+ e.detail = DND.DROP_NONE;
+ }
+
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
+ LayoutCanvas canvas = mOutlinePage.getEditor().getCanvasControl();
+ e.data = SelectionItem.getAsText(canvas, mDragSelection);
+ return;
+ }
+
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mDragElements;
+ return;
+ }
+
+ // otherwise we failed
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ }
+
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Unregister the dragged data.
+ // Clear the selection
+ mDragSelection.clear();
+ mDragElements = null;
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+ }
+
+ private CanvasViewInfo getViewInfo(TreeItem item) {
+ Object data = item.getData();
+ if (data != null) {
+ return OutlinePage.getViewInfo(data);
+ }
+
+ return null;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java
new file mode 100644
index 000000000..f4a826fa2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TransferData;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Drop listener for the outline page */
+/*package*/ class OutlineDropListener extends ViewerDropAdapter {
+ private final OutlinePage mOutlinePage;
+
+ public OutlineDropListener(OutlinePage outlinePage, TreeViewer treeViewer) {
+ super(treeViewer);
+ mOutlinePage = outlinePage;
+ }
+
+ @Override
+ public void dragEnter(DropTargetEvent event) {
+ if (event.detail == DND.DROP_NONE && GlobalCanvasDragInfo.getInstance().isDragging()) {
+ // For some inexplicable reason, we get DND.DROP_NONE from the palette
+ // even though in its drag start we set DND.DROP_COPY, so correct that here...
+ int operation = DND.DROP_COPY;
+ event.detail = operation;
+ }
+ super.dragEnter(event);
+ }
+
+ @Override
+ public boolean performDrop(Object data) {
+ final DropTargetEvent event = getCurrentEvent();
+ if (event == null) {
+ return false;
+ }
+ int location = determineLocation(event);
+ if (location == LOCATION_NONE) {
+ return false;
+ }
+
+ final SimpleElement[] elements;
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ if (sxt.isSupportedType(event.currentDataType)) {
+ if (data instanceof SimpleElement[]) {
+ elements = (SimpleElement[]) data;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ if (elements.length == 0) {
+ return false;
+ }
+
+ // Determine target:
+ CanvasViewInfo parent = OutlinePage.getViewInfo(event.item.getData());
+ if (parent == null) {
+ return false;
+ }
+
+ int index = -1;
+ UiViewElementNode parentNode = parent.getUiViewNode();
+ if (location == LOCATION_BEFORE || location == LOCATION_AFTER) {
+ UiViewElementNode node = parentNode;
+ parent = parent.getParent();
+ if (parent == null) {
+ return false;
+ }
+ parentNode = parent.getUiViewNode();
+
+ // Determine index
+ index = 0;
+ for (UiElementNode child : parentNode.getUiChildren()) {
+ if (child == node) {
+ break;
+ }
+ index++;
+ }
+ if (location == LOCATION_AFTER) {
+ index++;
+ }
+ }
+
+ // Copy into new position.
+ final LayoutCanvas canvas = mOutlinePage.getEditor().getCanvasControl();
+ final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
+
+ // Record children of the target right before the drop (such that we can
+ // find out after the drop which exact children were inserted)
+ Set<INode> children = new HashSet<INode>();
+ for (INode node : targetNode.getChildren()) {
+ children.add(node);
+ }
+
+ String label = MoveGesture.computeUndoLabel(targetNode, elements, event.detail);
+ final int indexFinal = index;
+ canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ InsertType insertType = MoveGesture.getInsertType(event, targetNode);
+ canvas.getRulesEngine().setInsertType(insertType);
+
+ Object sourceCanvas = GlobalCanvasDragInfo.getInstance().getSourceCanvas();
+ boolean createNew = event.detail == DND.DROP_COPY || sourceCanvas != canvas;
+ BaseLayoutRule.insertAt(targetNode, elements, createNew, indexFinal);
+ targetNode.applyPendingChanges();
+
+ // Clean up drag if applicable
+ if (event.detail == DND.DROP_MOVE) {
+ GlobalCanvasDragInfo.getInstance().removeSource();
+ }
+ }
+ });
+
+ // Now find out which nodes were added, and look up their corresponding
+ // CanvasViewInfos
+ final List<INode> added = new ArrayList<INode>();
+ for (INode node : targetNode.getChildren()) {
+ if (!children.contains(node)) {
+ added.add(node);
+ }
+ }
+ // Select the newly dropped nodes
+ final SelectionManager selectionManager = canvas.getSelectionManager();
+ selectionManager.setOutlineSelection(added);
+
+ canvas.redraw();
+
+ return true;
+ }
+
+ @Override
+ public boolean validateDrop(Object target, int operation,
+ TransferData transferType) {
+ DropTargetEvent event = getCurrentEvent();
+ if (event == null) {
+ return false;
+ }
+ int location = determineLocation(event);
+ if (location == LOCATION_NONE) {
+ return false;
+ }
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ if (!sxt.isSupportedType(transferType)) {
+ return false;
+ }
+
+ CanvasViewInfo parent = OutlinePage.getViewInfo(event.item.getData());
+ if (parent == null) {
+ return false;
+ }
+
+ UiViewElementNode parentNode = parent.getUiViewNode();
+
+ if (location == LOCATION_ON) {
+ // Targeting the middle of an item means to add it as a new child
+ // of the given element. This is only allowed on some types of nodes.
+ if (!DescriptorsUtils.canInsertChildren(parentNode.getDescriptor(),
+ parent.getViewObject())) {
+ return false;
+ }
+ }
+
+ // Check that the drop target position is not a child or identical to
+ // one of the dragged items
+ SelectionItem[] sel = GlobalCanvasDragInfo.getInstance().getCurrentSelection();
+ if (sel != null) {
+ for (SelectionItem item : sel) {
+ if (isAncestor(item.getViewInfo().getUiViewNode(), parentNode)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns true if the given parent node is an ancestor of the given child node */
+ private boolean isAncestor(UiElementNode parent, UiElementNode child) {
+ while (child != null) {
+ if (child == parent) {
+ return true;
+ }
+ child = child.getUiParent();
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java
new file mode 100644
index 000000000..e63fff7ab
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link OutlineOverlay} paints an optional outline on top of the layout,
+ * showing the structure of the individual Android View elements.
+ */
+public class OutlineOverlay extends Overlay {
+ /** The {@link ViewHierarchy} this outline visualizes */
+ private final ViewHierarchy mViewHierarchy;
+
+ /** Outline color. Must be disposed, it's NOT a system color. */
+ private Color mOutlineColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Constructs a new {@link OutlineOverlay} linked to the given view
+ * hierarchy.
+ *
+ * @param viewHierarchy The {@link ViewHierarchy} to render
+ * @param hScale The {@link CanvasTransform} to use to transfer horizontal layout
+ * coordinates to screen coordinates
+ * @param vScale The {@link CanvasTransform} to use to transfer vertical layout
+ * coordinates to screen coordinates
+ */
+ public OutlineOverlay(
+ ViewHierarchy viewHierarchy,
+ CanvasTransform hScale,
+ CanvasTransform vScale) {
+ super();
+ mViewHierarchy = viewHierarchy;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ mOutlineColor = new Color(device, SwtDrawingStyle.OUTLINE.getStrokeColor());
+ }
+
+ @Override
+ public void dispose() {
+ if (mOutlineColor != null) {
+ mOutlineColor.dispose();
+ mOutlineColor = null;
+ }
+ }
+
+ @Override
+ public void paint(GC gc) {
+ CanvasViewInfo lastRoot = mViewHierarchy.getRoot();
+ if (lastRoot != null) {
+ gc.setForeground(mOutlineColor);
+ gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle());
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha());
+ drawOutline(gc, lastRoot);
+ gc.setAlpha(oldAlpha);
+ }
+ }
+
+ private void drawOutline(GC gc, CanvasViewInfo info) {
+ Rectangle r = info.getAbsRect();
+
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.width);
+ int h = mVScale.scale(r.height);
+
+ // Add +1 to the width and +1 to the height such that when you have a
+ // series of boxes (in say a LinearLayout), instead of the bottom of one
+ // box and the top of the next box being -adjacent-, they -overlap-.
+ // This makes the outline nicer visually since you don't get
+ // "double thickness" lines for all adjacent boxes.
+ gc.drawRectangle(x, y, w + 1, h + 1);
+
+ for (CanvasViewInfo vi : info.getChildren()) {
+ drawOutline(gc, vi);
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
new file mode 100644
index 000000000..8178c6871
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
@@ -0,0 +1,1439 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
+import static com.android.SdkConstants.ATTR_ROW_COUNT;
+import static com.android.SdkConstants.ATTR_SRC;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.AUTO_URI;
+import static com.android.SdkConstants.DRAWABLE_PREFIX;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.URI_PREFIX;
+import static org.eclipse.jface.viewers.StyledString.COUNTER_STYLER;
+import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
+
+import com.android.SdkConstants;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.preference.JFacePreferences;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IElementComparer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StyledCellLabelProvider;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.jface.viewers.StyledString.Styler;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.INullSelectionListener;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
+import org.eclipse.wb.core.controls.SelfOrientingSashForm;
+import org.eclipse.wb.internal.core.editor.structure.IPage;
+import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An outline page for the layout canvas view.
+ * <p/>
+ * The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means
+ * we have *one* instance of the outline page per open canvas editor.
+ * <p/>
+ * It sets itself as a listener on the site's selection service in order to be
+ * notified of the canvas' selection changes.
+ * The underlying page is also a selection provider (via IContentOutlinePage)
+ * and as such it will broadcast selection changes to the site's selection service
+ * (on which both the layout editor part and the property sheet page listen.)
+ */
+public class OutlinePage extends ContentOutlinePage
+ implements INullSelectionListener, IPage {
+
+ /** Label which separates outline text from additional attributes like text prefix or url */
+ private static final String LABEL_SEPARATOR = " - ";
+
+ /** Max character count in labels, used for truncation */
+ private static final int LABEL_MAX_WIDTH = 50;
+
+ /**
+ * The graphical editor that created this outline.
+ */
+ private final GraphicalEditorPart mGraphicalEditorPart;
+
+ /**
+ * RootWrapper is a workaround: we can't set the input of the TreeView to its root
+ * element, so we introduce a fake parent.
+ */
+ private final RootWrapper mRootWrapper = new RootWrapper();
+
+ /**
+ * Menu manager for the context menu actions.
+ * The actions delegate to the current GraphicalEditorPart.
+ */
+ private MenuManager mMenuManager;
+
+ private Composite mControl;
+ private PropertySheetPage mPropertySheet;
+ private PageSiteComposite mPropertySheetComposite;
+ private boolean mShowPropertySheet;
+ private boolean mShowHeader;
+ private boolean mIgnoreSelection;
+ private boolean mActive = true;
+
+ /** Action to Select All in the tree */
+ private final Action mTreeSelectAllAction = new Action() {
+ @Override
+ public void run() {
+ getTreeViewer().getTree().selectAll();
+ OutlinePage.this.fireSelectionChanged(getSelection());
+ }
+
+ @Override
+ public String getId() {
+ return ActionFactory.SELECT_ALL.getId();
+ }
+ };
+
+ /** Action for moving items up in the tree */
+ private Action mMoveUpAction = new Action("Move Up\t-",
+ IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-1$
+
+ @Override
+ public String getId() {
+ return "adt.outline.moveup"; //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return canMove(false);
+ }
+
+ @Override
+ public void run() {
+ move(false);
+ }
+ };
+
+ /** Action for moving items down in the tree */
+ private Action mMoveDownAction = new Action("Move Down\t+",
+ IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$
+
+ @Override
+ public String getId() {
+ return "adt.outline.movedown"; //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return canMove(true);
+ }
+
+ @Override
+ public void run() {
+ move(true);
+ }
+ };
+
+ /**
+ * Creates a new {@link OutlinePage} associated with the given editor
+ *
+ * @param graphicalEditorPart the editor associated with this outline
+ */
+ public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
+ super();
+ mGraphicalEditorPart = graphicalEditorPart;
+ }
+
+ @Override
+ public Control getControl() {
+ // We've injected some controls between the root of the outline page
+ // and the tree control, so return the actual root (a sash form) rather
+ // than the superclass' implementation which returns the tree. If we don't
+ // do this, various checks in the outline page which checks that getControl().getParent()
+ // is the outline window itself will ignore this page.
+ return mControl;
+ }
+
+ void setActive(boolean active) {
+ if (active != mActive) {
+ mActive = active;
+
+ // Outlines are by default active when they are created; this is intended
+ // for deactivating a hidden outline and later reactivating it
+ assert mControl != null;
+ if (active) {
+ getSite().getPage().addSelectionListener(this);
+ setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot());
+ } else {
+ getSite().getPage().removeSelectionListener(this);
+ mRootWrapper.setRoot(null);
+ if (mPropertySheet != null) {
+ mPropertySheet.selectionChanged(null, TreeSelection.EMPTY);
+ }
+ }
+ }
+ }
+
+ /** Refresh all the icon state */
+ public void refreshIcons() {
+ TreeViewer treeViewer = getTreeViewer();
+ if (treeViewer != null) {
+ Tree tree = treeViewer.getTree();
+ if (tree != null && !tree.isDisposed()) {
+ treeViewer.refresh();
+ }
+ }
+ }
+
+ /**
+ * Set whether the outline should be shown in the header
+ *
+ * @param show whether a header should be shown
+ */
+ public void setShowHeader(boolean show) {
+ mShowHeader = show;
+ }
+
+ /**
+ * Set whether the property sheet should be shown within this outline
+ *
+ * @param show whether the property sheet should show
+ */
+ public void setShowPropertySheet(boolean show) {
+ if (show != mShowPropertySheet) {
+ mShowPropertySheet = show;
+ if (mControl == null) {
+ return;
+ }
+
+ if (show && mPropertySheet == null) {
+ createPropertySheet();
+ } else if (!show) {
+ mPropertySheetComposite.dispose();
+ mPropertySheetComposite = null;
+ mPropertySheet.dispose();
+ mPropertySheet = null;
+ }
+
+ mControl.layout();
+ }
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL);
+
+ if (mShowHeader) {
+ PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER);
+ mOutlineComposite.setTitleText("Outline");
+ mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view"));
+ mOutlineComposite.setPage(new IPage() {
+ @Override
+ public void createControl(Composite outlineParent) {
+ createOutline(outlineParent);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public Control getControl() {
+ return getTreeViewer().getTree();
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ makeContributions(null, toolBarManager, null);
+ toolBarManager.update(false);
+ }
+
+ @Override
+ public void setFocus() {
+ getControl().setFocus();
+ }
+ });
+ } else {
+ createOutline(mControl);
+ }
+
+ if (mShowPropertySheet) {
+ createPropertySheet();
+ }
+ }
+
+ private void createOutline(Composite parent) {
+ if (AdtUtils.isEclipse4()) {
+ // This is a workaround for the focus behavior in Eclipse 4 where
+ // the framework ends up calling setFocus() on the first widget in the outline
+ // AFTER a mouse click has been received. Specifically, if the user clicks in
+ // the embedded property sheet to for example give a Text property editor focus,
+ // then after the mouse click, the Outline window activation event is processed,
+ // and this event causes setFocus() to be called first on the PageBookView (which
+ // ends up calling setFocus on the first control, normally the TreeViewer), and
+ // then on the Page itself. We're dealing with the page setFocus() in the override
+ // of that method in the class, such that it does nothing.
+ // However, we have to also disable the setFocus on the first control in the
+ // outline page. To deal with that, we create our *own* first control in the
+ // outline, and make its setFocus() a no-op. We also make it invisible, since we
+ // don't actually want anything but the tree viewer showing in the outline.
+ Text text = new Text(parent, SWT.NONE) {
+ @Override
+ public boolean setFocus() {
+ // Focus no-op
+ return true;
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+ };
+ text.setVisible(false);
+ }
+
+ super.createControl(parent);
+
+ TreeViewer tv = getTreeViewer();
+ tv.setAutoExpandLevel(2);
+ tv.setContentProvider(new ContentProvider());
+ tv.setLabelProvider(new LabelProvider());
+ tv.setInput(mRootWrapper);
+ tv.expandToLevel(mRootWrapper.getRoot(), 2);
+
+ int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE;
+ Transfer[] transfers = new Transfer[] {
+ SimpleXmlTransfer.getInstance()
+ };
+
+ tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv));
+ tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv));
+
+ // The tree viewer will hold CanvasViewInfo instances, however these
+ // change each time the canvas is reloaded. OTOH layoutlib gives us
+ // constant UiView keys which we can use to perform tree item comparisons.
+ tv.setComparer(new IElementComparer() {
+ @Override
+ public int hashCode(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode();
+ if (key != null) {
+ return key.hashCode();
+ }
+ }
+ if (element != null) {
+ return element.hashCode();
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object a, Object b) {
+ if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) {
+ UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode();
+ UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode();
+ if (keyA != null) {
+ return keyA.equals(keyB);
+ }
+ }
+ if (a != null) {
+ return a.equals(b);
+ }
+ return false;
+ }
+ });
+ tv.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ // This used to open the property view, but now that properties are docked
+ // let's use it for something else -- such as showing the editor source
+ /*
+ // Front properties panel; its selection is already linked
+ IWorkbenchPage page = getSite().getPage();
+ try {
+ page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Could not activate property sheet");
+ }
+ */
+
+ TreeItem[] selection = getTreeViewer().getTree().getSelection();
+ if (selection.length > 0) {
+ CanvasViewInfo vi = getViewInfo(selection[0].getData());
+ if (vi != null) {
+ LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
+ canvas.show(vi);
+ }
+ }
+ }
+ });
+
+ setupContextMenu();
+
+ // Listen to selection changes from the layout editor
+ getSite().getPage().addSelectionListener(this);
+ getControl().addDisposeListener(new DisposeListener() {
+
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ dispose();
+ }
+ });
+
+ Tree tree = tv.getTree();
+ tree.addKeyListener(new KeyListener() {
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.character == '-') {
+ if (mMoveUpAction.isEnabled()) {
+ mMoveUpAction.run();
+ }
+ } else if (e.character == '+') {
+ if (mMoveDownAction.isEnabled()) {
+ mMoveDownAction.run();
+ }
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+ });
+
+ setupTooltip();
+ }
+
+ /**
+ * This flag is true when the mouse button is being pressed somewhere inside
+ * the property sheet
+ */
+ private boolean mPressInPropSheet;
+
+ private void createPropertySheet() {
+ mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER);
+ mPropertySheetComposite.setTitleText("Properties");
+ mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view"));
+ mPropertySheet = new PropertySheetPage(mGraphicalEditorPart);
+ mPropertySheetComposite.setPage(mPropertySheet);
+ if (AdtUtils.isEclipse4()) {
+ mPropertySheet.getControl().addMouseListener(new MouseListener() {
+ @Override
+ public void mouseDown(MouseEvent e) {
+ mPressInPropSheet = true;
+ }
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ mPressInPropSheet = false;
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setFocus() {
+ // Only call setFocus on the tree viewer if the mouse click isn't in the property
+ // sheet area
+ if (!mPressInPropSheet) {
+ super.setFocus();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ mRootWrapper.setRoot(null);
+
+ getSite().getPage().removeSelectionListener(this);
+ super.dispose();
+ if (mPropertySheet != null) {
+ mPropertySheet.dispose();
+ mPropertySheet = null;
+ }
+ }
+
+ /**
+ * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
+ *
+ * @param rootViewInfo The root of the view info hierarchy. Can be null.
+ */
+ public void setModel(CanvasViewInfo rootViewInfo) {
+ if (!mActive) {
+ return;
+ }
+
+ mRootWrapper.setRoot(rootViewInfo);
+
+ TreeViewer tv = getTreeViewer();
+ if (tv != null && !tv.getTree().isDisposed()) {
+ Object[] expanded = tv.getExpandedElements();
+ tv.refresh();
+ tv.setExpandedElements(expanded);
+ // Ensure that the root is expanded
+ tv.expandToLevel(rootViewInfo, 2);
+ }
+ }
+
+ /**
+ * Returns the current tree viewer selection. Shouldn't be null,
+ * although it can be {@link TreeSelection#EMPTY}.
+ */
+ @Override
+ public ISelection getSelection() {
+ return super.getSelection();
+ }
+
+ /**
+ * Sets the outline selection.
+ *
+ * @param selection Only {@link ITreeSelection} will be used, otherwise the
+ * selection will be cleared (including a null selection).
+ */
+ @Override
+ public void setSelection(ISelection selection) {
+ // TreeViewer should be able to deal with a null selection, but let's make it safe
+ if (selection == null) {
+ selection = TreeSelection.EMPTY;
+ }
+ if (selection.equals(TreeSelection.EMPTY)) {
+ return;
+ }
+
+ super.setSelection(selection);
+
+ TreeViewer tv = getTreeViewer();
+ if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) {
+ return;
+ }
+
+ // auto-reveal the selection
+ ITreeSelection treeSel = (ITreeSelection) selection;
+ for (TreePath p : treeSel.getPaths()) {
+ tv.expandToLevel(p, 1);
+ }
+ }
+
+ @Override
+ protected void fireSelectionChanged(ISelection selection) {
+ super.fireSelectionChanged(selection);
+ if (mPropertySheet != null && !mIgnoreSelection) {
+ mPropertySheet.selectionChanged(null, selection);
+ }
+ }
+
+ /**
+ * Listens to a workbench selection.
+ * Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid
+ * picking up our own selections.
+ */
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ if (mIgnoreSelection) {
+ return;
+ }
+
+ if (part instanceof IEditorPart) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part);
+ if (delegate != null) {
+ try {
+ mIgnoreSelection = true;
+ setSelection(selection);
+
+ if (mPropertySheet != null) {
+ mPropertySheet.selectionChanged(part, selection);
+ }
+ } finally {
+ mIgnoreSelection = false;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ if (!mIgnoreSelection) {
+ super.selectionChanged(event);
+ }
+ }
+
+ // ----
+
+ /**
+ * In theory, the root of the model should be the input of the {@link TreeViewer},
+ * which would be the root {@link CanvasViewInfo}.
+ * That means in theory {@link ContentProvider#getElements(Object)} should return
+ * its own input as the single root node.
+ * <p/>
+ * However as described in JFace Bug 9262, this case is not properly handled by
+ * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer.
+ * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262
+ * <p/>
+ * The solution is to wrap the tree viewer input in a dummy root node that acts
+ * as a parent. This class does just that.
+ */
+ private static class RootWrapper {
+ private CanvasViewInfo mRoot;
+
+ public void setRoot(CanvasViewInfo root) {
+ mRoot = root;
+ }
+
+ public CanvasViewInfo getRoot() {
+ return mRoot;
+ }
+ }
+
+ /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
+ /* package */ static CanvasViewInfo getViewInfo(Object viewData) {
+ if (viewData instanceof RootWrapper) {
+ return ((RootWrapper) viewData).getRoot();
+ }
+ if (viewData instanceof CanvasViewInfo) {
+ return (CanvasViewInfo) viewData;
+ }
+ return null;
+ }
+
+ // --- Content and Label Providers ---
+
+ /**
+ * Content provider for the Outline model.
+ * Objects are going to be {@link CanvasViewInfo}.
+ */
+ private static class ContentProvider implements ITreeContentProvider {
+
+ @Override
+ public Object[] getChildren(Object element) {
+ if (element instanceof RootWrapper) {
+ CanvasViewInfo root = ((RootWrapper)element).getRoot();
+ if (root != null) {
+ return new Object[] { root };
+ }
+ }
+ if (element instanceof CanvasViewInfo) {
+ List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren();
+ if (children != null) {
+ return children.toArray();
+ }
+ }
+ return new Object[0];
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ return ((CanvasViewInfo) element).getParent();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
+ if (children != null) {
+ return children.size() > 0;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the root element.
+ * Semantically, the root element is the single top-level XML element of the XML layout.
+ */
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return getChildren(inputElement);
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * Label provider for the Outline model.
+ * Objects are going to be {@link CanvasViewInfo}.
+ */
+ private class LabelProvider extends StyledCellLabelProvider {
+ /**
+ * Returns the element's logo with a fallback on the android logo.
+ *
+ * @param element the tree element
+ * @return the image to be used as a logo
+ */
+ public Image getImage(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ element = ((CanvasViewInfo) element).getUiViewNode();
+ }
+
+ if (element instanceof UiViewElementNode) {
+ UiViewElementNode v = (UiViewElementNode) element;
+ return v.getIcon();
+ }
+
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Uses {@link UiElementNode#getStyledDescription} for the label for this tree item.
+ */
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ StyledString styledString = null;
+
+ CanvasViewInfo vi = null;
+ if (element instanceof CanvasViewInfo) {
+ vi = (CanvasViewInfo) element;
+ element = vi.getUiViewNode();
+ }
+
+ Image image = getImage(element);
+
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ styledString = node.getStyledDescription();
+ Node xmlNode = node.getXmlNode();
+ if (xmlNode instanceof Element) {
+ Element e = (Element) xmlNode;
+
+ // Temporary diagnostics code when developing GridLayout
+ if (GridLayoutRule.sDebugGridLayout) {
+
+ String namespace;
+ if (e.getNodeName().equals(GRID_LAYOUT) ||
+ e.getParentNode() != null
+ && e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
+ namespace = ANDROID_URI;
+ } else {
+ // Else: probably a v7 gridlayout
+ IProject project = mGraphicalEditorPart.getProject();
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null && projectState.isLibrary()) {
+ namespace = AUTO_URI;
+ } else {
+ ManifestInfo info = ManifestInfo.get(project);
+ namespace = URI_PREFIX + info.getPackage();
+ }
+ }
+
+ if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) {
+ // Attach rowCount/columnCount info
+ String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT);
+ if (rowCount.length() == 0) {
+ rowCount = "?";
+ }
+ String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT);
+ if (columnCount.length() == 0) {
+ columnCount = "?";
+ }
+
+ styledString.append(" - columnCount=", QUALIFIER_STYLER);
+ styledString.append(columnCount, QUALIFIER_STYLER);
+ styledString.append(", rowCount=", QUALIFIER_STYLER);
+ styledString.append(rowCount, QUALIFIER_STYLER);
+ } else if (e.getParentNode() != null
+ && e.getParentNode().getNodeName() != null
+ && e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) {
+ // Attach row/column info
+ String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW);
+ if (row.length() == 0) {
+ row = "?";
+ }
+ Styler colStyle = QUALIFIER_STYLER;
+ String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN);
+ if (column.length() == 0) {
+ column = "?";
+ } else {
+ String colCount = ((Element) e.getParentNode()).getAttributeNS(
+ namespace, ATTR_COLUMN_COUNT);
+ if (colCount.length() > 0 && Integer.parseInt(colCount) <=
+ Integer.parseInt(column)) {
+ colStyle = StyledString.createColorRegistryStyler(
+ JFacePreferences.ERROR_COLOR, null);
+ }
+ }
+ String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN);
+ String columnSpan = e.getAttributeNS(namespace,
+ ATTR_LAYOUT_COLUMN_SPAN);
+ if (rowSpan.length() == 0) {
+ rowSpan = "1";
+ }
+ if (columnSpan.length() == 0) {
+ columnSpan = "1";
+ }
+
+ styledString.append(" - cell (row=", QUALIFIER_STYLER);
+ styledString.append(row, QUALIFIER_STYLER);
+ styledString.append(',', QUALIFIER_STYLER);
+ styledString.append("col=", colStyle);
+ styledString.append(column, colStyle);
+ styledString.append(')', colStyle);
+ styledString.append(", span=(", QUALIFIER_STYLER);
+ styledString.append(columnSpan, QUALIFIER_STYLER);
+ styledString.append(',', QUALIFIER_STYLER);
+ styledString.append(rowSpan, QUALIFIER_STYLER);
+ styledString.append(')', QUALIFIER_STYLER);
+
+ String gravity = e.getAttributeNS(namespace, ATTR_LAYOUT_GRAVITY);
+ if (gravity != null && gravity.length() > 0) {
+ styledString.append(" : ", COUNTER_STYLER);
+ styledString.append(gravity, COUNTER_STYLER);
+ }
+
+ }
+ }
+
+ if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
+ // Show the text attribute
+ String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT);
+ if (text != null && text.length() > 0
+ && !text.contains(node.getDescriptor().getUiName())) {
+ if (text.charAt(0) == '@') {
+ String resolved = mGraphicalEditorPart.findString(text);
+ if (resolved != null) {
+ text = resolved;
+ }
+ }
+ if (styledString.length() < LABEL_MAX_WIDTH - LABEL_SEPARATOR.length()
+ - 2) {
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+
+ styledString.append('"', QUALIFIER_STYLER);
+ styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
+ styledString.append('"', QUALIFIER_STYLER);
+ }
+ }
+ } else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
+ // Show ImageView source attributes etc
+ String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC);
+ if (src != null && src.length() > 0) {
+ if (src.startsWith(DRAWABLE_PREFIX)) {
+ src = src.substring(DRAWABLE_PREFIX.length());
+ }
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+ styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
+ }
+ } else if (e.getTagName().equals(SdkConstants.VIEW_INCLUDE)) {
+ // Show the include reference.
+
+ // Note: the layout attribute is NOT in the Android namespace
+ String src = e.getAttribute(SdkConstants.ATTR_LAYOUT);
+ if (src != null && src.length() > 0) {
+ if (src.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ src = src.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+ styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
+ }
+ }
+ }
+ } else if (element == null && vi != null) {
+ // It's an inclusion-context: display it
+ Reference includedWithin = mGraphicalEditorPart.getIncludedWithin();
+ if (includedWithin != null) {
+ styledString = new StyledString();
+ styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER);
+ image = IconFactory.getInstance().getIcon(SdkConstants.VIEW_INCLUDE);
+ }
+ }
+
+ if (styledString == null) {
+ styledString = new StyledString();
+ styledString.append(element == null ? "(null)" : element.toString());
+ }
+
+ cell.setText(styledString.toString());
+ cell.setStyleRanges(styledString.getStyleRanges());
+ cell.setImage(image);
+ super.update(cell);
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ return super.isLabelProperty(element, property);
+ }
+ }
+
+ // --- Context Menu ---
+
+ /**
+ * This viewer uses its own actions that delegate to the ones given
+ * by the {@link LayoutCanvas}. All the processing is actually handled
+ * directly by the canvas and this viewer only gets refreshed as a
+ * consequence of the canvas changing the XML model.
+ */
+ private void setupContextMenu() {
+
+ mMenuManager = new MenuManager();
+ mMenuManager.removeAll();
+
+ mMenuManager.add(mMoveUpAction);
+ mMenuManager.add(mMoveDownAction);
+ mMenuManager.add(new Separator());
+
+ mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart));
+ mMenuManager.add(new Separator());
+ final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION;
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId()));
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId()));
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId()));
+
+ mMenuManager.add(new Separator());
+
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId()));
+
+ mMenuManager.addMenuListener(new IMenuListener() {
+ @Override
+ public void menuAboutToShow(IMenuManager manager) {
+ // Update all actions to match their LayoutCanvas counterparts
+ for (IContributionItem contrib : manager.getItems()) {
+ if (contrib instanceof ActionContributionItem) {
+ IAction action = ((ActionContributionItem) contrib).getAction();
+ if (action instanceof DelegateAction) {
+ ((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart);
+ }
+ }
+ }
+ }
+ });
+
+ new DynamicContextMenu(
+ mGraphicalEditorPart.getEditorDelegate(),
+ mGraphicalEditorPart.getCanvasControl(),
+ mMenuManager);
+
+ getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl()));
+
+ // Update Move Up/Move Down state only when the menu is opened
+ getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() {
+ @Override
+ public void menuDetected(MenuDetectEvent e) {
+ mMenuManager.update(IAction.ENABLED);
+ }
+ });
+ }
+
+ /**
+ * An action that delegates its properties and behavior to a target action.
+ * The target action can be null or it can change overtime, typically as the
+ * layout canvas' editor part is activated or closed.
+ */
+ private static class DelegateAction extends Action {
+ private IAction mTargetAction;
+ private final String mCanvasActionId;
+
+ public DelegateAction(String canvasActionId) {
+ super(canvasActionId);
+ setId(canvasActionId);
+ mCanvasActionId = canvasActionId;
+ }
+
+ // --- Methods form IAction ---
+
+ /** Returns the target action's {@link #isEnabled()} if defined, or false. */
+ @Override
+ public boolean isEnabled() {
+ return mTargetAction == null ? false : mTargetAction.isEnabled();
+ }
+
+ /** Returns the target action's {@link #isChecked()} if defined, or false. */
+ @Override
+ public boolean isChecked() {
+ return mTargetAction == null ? false : mTargetAction.isChecked();
+ }
+
+ /** Returns the target action's {@link #isHandled()} if defined, or false. */
+ @Override
+ public boolean isHandled() {
+ return mTargetAction == null ? false : mTargetAction.isHandled();
+ }
+
+ /** Runs the target action if defined. */
+ @Override
+ public void run() {
+ if (mTargetAction != null) {
+ mTargetAction.run();
+ }
+ super.run();
+ }
+
+ /**
+ * Updates this action to delegate to its counterpart in the given editor part
+ *
+ * @param editorPart The editor being updated
+ */
+ public void updateFromEditorPart(GraphicalEditorPart editorPart) {
+ LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl();
+ if (canvas == null) {
+ mTargetAction = null;
+ } else {
+ mTargetAction = canvas.getAction(mCanvasActionId);
+ }
+
+ if (mTargetAction != null) {
+ setText(mTargetAction.getText());
+ setId(mTargetAction.getId());
+ setDescription(mTargetAction.getDescription());
+ setImageDescriptor(mTargetAction.getImageDescriptor());
+ setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor());
+ setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor());
+ setToolTipText(mTargetAction.getToolTipText());
+ setActionDefinitionId(mTargetAction.getActionDefinitionId());
+ setHelpListener(mTargetAction.getHelpListener());
+ setAccelerator(mTargetAction.getAccelerator());
+ setChecked(mTargetAction.isChecked());
+ setEnabled(mTargetAction.isEnabled());
+ } else {
+ setEnabled(false);
+ }
+ }
+ }
+
+ /** Returns the associated editor with this outline */
+ /* package */GraphicalEditorPart getEditor() {
+ return mGraphicalEditorPart;
+ }
+
+ @Override
+ public void setActionBars(IActionBars actionBars) {
+ super.setActionBars(actionBars);
+
+ // Map Outline actions to canvas actions such that they share Undo context etc
+ LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
+ canvas.updateGlobalActions(actionBars);
+
+ // Special handling for Select All since it's different than the canvas (will
+ // include selecting the root etc)
+ actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction);
+ actionBars.updateActionBars();
+ }
+
+ // ---- Move Up/Down Support ----
+
+ /** Returns true if the current selected item can be moved */
+ private boolean canMove(boolean forward) {
+ CanvasViewInfo viewInfo = getSingleSelectedItem();
+ if (viewInfo != null) {
+ UiViewElementNode node = viewInfo.getUiViewNode();
+ if (forward) {
+ return findNext(node) != null;
+ } else {
+ return findPrevious(node) != null;
+ }
+ }
+
+ return false;
+ }
+
+ /** Moves the current selected item down (forward) or up (not forward) */
+ private void move(boolean forward) {
+ CanvasViewInfo viewInfo = getSingleSelectedItem();
+ if (viewInfo != null) {
+ final Pair<UiViewElementNode, Integer> target;
+ UiViewElementNode selected = viewInfo.getUiViewNode();
+ if (forward) {
+ target = findNext(selected);
+ } else {
+ target = findPrevious(selected);
+ }
+ if (target != null) {
+ final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
+ final SelectionManager selectionManager = canvas.getSelectionManager();
+ final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>();
+ dragSelection.add(selectionManager.createSelection(viewInfo));
+ SelectionManager.sanitize(dragSelection);
+
+ if (!dragSelection.isEmpty()) {
+ final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection);
+ UiViewElementNode parentNode = target.getFirst();
+ final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
+
+ // Record children of the target right before the drop (such that we
+ // can find out after the drop which exact children were inserted)
+ Set<INode> children = new HashSet<INode>();
+ for (INode node : targetNode.getChildren()) {
+ children.add(node);
+ }
+
+ String label = MoveGesture.computeUndoLabel(targetNode,
+ elements, DND.DROP_MOVE);
+ canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ InsertType insertType = InsertType.MOVE_INTO;
+ if (dragSelection.get(0).getNode().getParent() == targetNode) {
+ insertType = InsertType.MOVE_WITHIN;
+ }
+ canvas.getRulesEngine().setInsertType(insertType);
+ int index = target.getSecond();
+ BaseLayoutRule.insertAt(targetNode, elements, false, index);
+ targetNode.applyPendingChanges();
+ canvas.getClipboardSupport().deleteSelection("Remove", dragSelection);
+ }
+ });
+
+ // Now find out which nodes were added, and look up their
+ // corresponding CanvasViewInfos
+ final List<INode> added = new ArrayList<INode>();
+ for (INode node : targetNode.getChildren()) {
+ if (!children.contains(node)) {
+ added.add(node);
+ }
+ }
+
+ selectionManager.setOutlineSelection(added);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link CanvasViewInfo} for the currently selected item, or null if
+ * there are no or multiple selected items
+ *
+ * @return the current selected item if there is exactly one item selected
+ */
+ private CanvasViewInfo getSingleSelectedItem() {
+ TreeItem[] selection = getTreeViewer().getTree().getSelection();
+ if (selection.length == 1) {
+ return getViewInfo(selection[0].getData());
+ }
+
+ return null;
+ }
+
+
+ /** Returns the pair [parent,index] of the next node (when iterating forward) */
+ @VisibleForTesting
+ /* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) {
+ UiElementNode parent = node.getUiParent();
+ if (parent == null) {
+ return null;
+ }
+
+ UiElementNode next = node.getUiNextSibling();
+ if (next != null) {
+ if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) {
+ return getFirstPosition(next);
+ } else {
+ return getPositionAfter(next);
+ }
+ }
+
+ next = parent.getUiNextSibling();
+ if (next != null) {
+ return getPositionBefore(next);
+ } else {
+ UiElementNode grandParent = parent.getUiParent();
+ if (grandParent != null) {
+ return getLastPosition(grandParent);
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the pair [parent,index] of the previous node (when iterating backward) */
+ @VisibleForTesting
+ /* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) {
+ UiElementNode prev = node.getUiPreviousSibling();
+ if (prev != null) {
+ UiElementNode curr = prev;
+ while (true) {
+ List<UiElementNode> children = curr.getUiChildren();
+ if (children.size() > 0) {
+ curr = children.get(children.size() - 1);
+ continue;
+ }
+ if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) {
+ return getFirstPosition(curr);
+ } else {
+ if (curr == prev) {
+ return getPositionBefore(curr);
+ } else {
+ return getPositionAfter(curr);
+ }
+ }
+ }
+ }
+
+ return getPositionBefore(node.getUiParent());
+ }
+
+ /** Returns the pair [parent,index] of the position immediately before the given node */
+ private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) {
+ if (node != null) {
+ UiElementNode parent = node.getUiParent();
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex());
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the pair [parent,index] of the position immediately following the given node */
+ private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) {
+ if (node != null) {
+ UiElementNode parent = node.getUiParent();
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1);
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the pair [parent,index] of the first position inside the given parent */
+ private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) {
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, 0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the pair [parent,index] of the last position after the given node's
+ * children
+ */
+ private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) {
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size());
+ }
+
+ return null;
+ }
+
+ /**
+ * Truncates the given text such that it will fit into the given {@link StyledString}
+ * up to a maximum length of {@link #LABEL_MAX_WIDTH}.
+ *
+ * @param text the text to truncate
+ * @param string the existing string to be appended to
+ * @return the truncated string
+ */
+ private static String truncate(String text, StyledString string) {
+ int existingLength = string.length();
+
+ if (text.length() + existingLength > LABEL_MAX_WIDTH) {
+ int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3;
+ if (truncatedLength > 0) {
+ return String.format("%1$s...", text.substring(0, truncatedLength));
+ } else {
+ return ""; //$NON-NLS-1$
+ }
+ }
+
+ return text;
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ makeContributions(null, toolBarManager, null);
+ toolBarManager.update(false);
+ }
+
+ /**
+ * Sets up a custom tooltip when hovering over tree items. It currently displays the error
+ * message for the lint warning associated with each node, if any (and only if the hover
+ * is over the icon portion).
+ */
+ private void setupTooltip() {
+ final Tree tree = getTreeViewer().getTree();
+
+ // This is based on SWT Snippet 125
+ final Listener listener = new Listener() {
+ Shell mTip = null;
+ Label mLabel = null;
+
+ @Override
+ public void handleEvent(Event event) {
+ switch(event.type) {
+ case SWT.Dispose:
+ case SWT.KeyDown:
+ case SWT.MouseExit:
+ case SWT.MouseDown:
+ case SWT.MouseMove:
+ if (mTip != null) {
+ mTip.dispose();
+ mTip = null;
+ mLabel = null;
+ }
+ break;
+ case SWT.MouseHover:
+ if (mTip != null) {
+ mTip.dispose();
+ mTip = null;
+ mLabel = null;
+ }
+
+ String tooltip = null;
+
+ TreeItem item = tree.getItem(new Point(event.x, event.y));
+ if (item != null) {
+ Rectangle rect = item.getBounds(0);
+ if (event.x - rect.x > 16) { // 16: Standard width of our outline icons
+ return;
+ }
+
+ Object data = item.getData();
+ if (data != null && data instanceof CanvasViewInfo) {
+ LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate();
+ CanvasViewInfo vi = (CanvasViewInfo) data;
+ IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
+ if (marker != null) {
+ tooltip = marker.getAttribute(IMarker.MESSAGE, null);
+ }
+ }
+
+ if (tooltip != null) {
+ Shell shell = tree.getShell();
+ Display display = tree.getDisplay();
+
+ Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
+ Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
+ mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
+ mTip.setBackground(bg);
+ FillLayout layout = new FillLayout();
+ layout.marginWidth = 1;
+ layout.marginHeight = 1;
+ mTip.setLayout(layout);
+ mLabel = new Label(mTip, SWT.WRAP);
+ mLabel.setForeground(fg);
+ mLabel.setBackground(bg);
+ mLabel.setText(tooltip);
+ mLabel.addListener(SWT.MouseExit, this);
+ mLabel.addListener(SWT.MouseDown, this);
+
+ Point pt = tree.toDisplay(rect.x, rect.y + rect.height);
+ Rectangle displayBounds = display.getBounds();
+ // -10: Don't extend -all- the way to the edge of the screen
+ // which would make it look like it has been cropped
+ int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10;
+ if (availableWidth < 80) {
+ availableWidth = 80;
+ }
+ Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ if (size.x > availableWidth) {
+ size = mTip.computeSize(availableWidth, SWT.DEFAULT);
+ }
+ mTip.setBounds(pt.x, pt.y, size.x, size.y);
+
+ mTip.setVisible(true);
+ }
+ }
+ }
+ }
+ };
+
+ tree.addListener(SWT.Dispose, listener);
+ tree.addListener(SWT.KeyDown, listener);
+ tree.addListener(SWT.MouseMove, listener);
+ tree.addListener(SWT.MouseHover, listener);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java
new file mode 100644
index 000000000..9b7e0eb18
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+/**
+ * An Overlay is a set of graphics which can be painted on top of the visual
+ * editor. Different {@link Gesture}s produce context specific overlays, such as
+ * swiping rectangles from the {@link MarqueeGesture} and guidelines from the
+ * {@link MoveGesture}.
+ */
+public abstract class Overlay {
+ private Device mDevice;
+
+ /** Whether the hover is hidden */
+ private boolean mHiding;
+
+ /**
+ * Construct the overlay, using the given graphics context for painting.
+ */
+ public Overlay() {
+ super();
+ }
+
+ /**
+ * Initializes the overlay before the first use, if applicable. This is a
+ * good place to initialize resources like colors.
+ *
+ * @param device The device to allocate resources for; the parameter passed
+ * to {@link #paint} will correspond to this device.
+ */
+ public void create(Device device) {
+ mDevice = device;
+ }
+
+ /**
+ * Releases resources held by the overlay. Called by the editor when an
+ * overlay has been removed.
+ */
+ public void dispose() {
+ }
+
+ /**
+ * Paints the overlay.
+ *
+ * @param gc The SWT {@link GC} object to draw into.
+ */
+ public void paint(GC gc) {
+ throw new IllegalArgumentException("paint() not implemented, probably done "
+ + "with specialized paint signature");
+ }
+
+ /** Returns the device associated with this overlay */
+ public Device getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns whether the overlay is hidden
+ *
+ * @return true if the selection overlay is hidden
+ */
+ public boolean isHiding() {
+ return mHiding;
+ }
+
+ /**
+ * Hides the overlay
+ *
+ * @param hiding true to hide the overlay, false to unhide it (default)
+ */
+ public void setHiding(boolean hiding) {
+ mHiding = hiding;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
new file mode 100644
index 000000000..46168b70f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
@@ -0,0 +1,1265 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.XMLNS_ANDROID;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.wb.internal.core.editor.structure.IPage;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A palette control for the {@link GraphicalEditorPart}.
+ * <p/>
+ * The palette contains several groups, each with a UI name (e.g. layouts and views) and each
+ * with a list of element descriptors.
+ * <p/>
+ *
+ * TODO list:
+ * - The available items should depend on the actual GLE2 Canvas selection. Selected android
+ * views should force filtering on what they accept can be dropped on them (e.g. TabHost,
+ * TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.
+ * - Optional: a text filter
+ * - Optional: have context-sensitive tools items, e.g. selection arrow tool,
+ * group selection tool, alignment, etc.
+ */
+public class PaletteControl extends Composite {
+
+ /**
+ * Wrapper to create a {@link PaletteControl}
+ */
+ static class PalettePage implements IPage {
+ private final GraphicalEditorPart mEditorPart;
+ private PaletteControl mControl;
+
+ PalettePage(GraphicalEditorPart editor) {
+ mEditorPart = editor;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mControl = new PaletteControl(parent, mEditorPart);
+ }
+
+ @Override
+ public Control getControl() {
+ return mControl;
+ }
+
+ @Override
+ public void dispose() {
+ mControl.dispose();
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ }
+
+ /**
+ * Add tool bar items to the given toolbar
+ *
+ * @param toolbar the toolbar to add items into
+ */
+ void createToolbarItems(final ToolBar toolbar) {
+ final ToolItem popupMenuItem = new ToolItem(toolbar, SWT.PUSH);
+ popupMenuItem.setToolTipText("View Menu");
+ popupMenuItem.setImage(IconFactory.getInstance().getIcon("view_menu"));
+ popupMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Rectangle bounds = popupMenuItem.getBounds();
+ // Align menu horizontally with the toolbar button and
+ // vertically with the bottom of the toolbar
+ Point point = toolbar.toDisplay(bounds.x, bounds.y + bounds.height);
+ mControl.showMenu(point.x, point.y);
+ }
+ });
+ }
+
+ @Override
+ public void setFocus() {
+ mControl.setFocus();
+ }
+ }
+
+ /**
+ * The parent grid layout that contains all the {@link Toggle} and
+ * {@link IconTextItem} widgets.
+ */
+ private GraphicalEditorPart mEditor;
+ private Color mBackground;
+ private Color mForeground;
+
+ /** The palette modes control various ways to visualize and lay out the views */
+ private static enum PaletteMode {
+ /** Show rendered previews of the views */
+ PREVIEW("Show Previews", true),
+ /** Show rendered previews of the views, scaled down to 75% */
+ SMALL_PREVIEW("Show Small Previews", true),
+ /** Show rendered previews of the views, scaled down to 50% */
+ TINY_PREVIEW("Show Tiny Previews", true),
+ /** Show an icon + text label */
+ ICON_TEXT("Show Icon and Text", false),
+ /** Show only icons, packed multiple per row */
+ ICON_ONLY("Show Only Icons", true);
+
+ PaletteMode(String actionLabel, boolean wrap) {
+ mActionLabel = actionLabel;
+ mWrap = wrap;
+ }
+
+ public String getActionLabel() {
+ return mActionLabel;
+ }
+
+ public boolean getWrap() {
+ return mWrap;
+ }
+
+ public boolean isPreview() {
+ return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW;
+ }
+
+ public boolean isScaledPreview() {
+ return this == SMALL_PREVIEW || this == TINY_PREVIEW;
+ }
+
+ private final String mActionLabel;
+ private final boolean mWrap;
+ };
+
+ /** Token used in preference string to record alphabetical sorting */
+ private static final String VALUE_ALPHABETICAL = "alpha"; //$NON-NLS-1$
+ /** Token used in preference string to record categories being turned off */
+ private static final String VALUE_NO_CATEGORIES = "nocat"; //$NON-NLS-1$
+ /** Token used in preference string to record auto close being turned off */
+ private static final String VALUE_NO_AUTOCLOSE = "noauto"; //$NON-NLS-1$
+
+ private final PreviewIconFactory mPreviewIconFactory = new PreviewIconFactory(this);
+ private PaletteMode mPaletteMode = null;
+ /** Use alphabetical sorting instead of natural order? */
+ private boolean mAlphabetical;
+ /** Use categories instead of a single large list of views? */
+ private boolean mCategories = true;
+ /** Auto-close the previous category when new categories are opened */
+ private boolean mAutoClose = true;
+ private AccordionControl mAccordion;
+ private String mCurrentTheme;
+ private String mCurrentDevice;
+ private IAndroidTarget mCurrentTarget;
+ private AndroidTargetData mCurrentTargetData;
+
+ /**
+ * Create the composite.
+ * @param parent The parent composite.
+ * @param editor An editor associated with this palette.
+ */
+ public PaletteControl(Composite parent, GraphicalEditorPart editor) {
+ super(parent, SWT.NONE);
+
+ mEditor = editor;
+ }
+
+ /** Reads UI mode from persistent store to preserve palette mode across IDE sessions */
+ private void loadPaletteMode() {
+ String paletteModes = AdtPrefs.getPrefs().getPaletteModes();
+ if (paletteModes.length() > 0) {
+ String[] tokens = paletteModes.split(","); //$NON-NLS-1$
+ try {
+ mPaletteMode = PaletteMode.valueOf(tokens[0]);
+ } catch (Throwable t) {
+ mPaletteMode = PaletteMode.values()[0];
+ }
+ mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL);
+ mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES);
+ mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE);
+ } else {
+ mPaletteMode = PaletteMode.SMALL_PREVIEW;
+ }
+ }
+
+ /**
+ * Returns the most recently stored version of auto-close-mode; this is the last
+ * user-initiated setting of the auto-close mode (we programmatically switch modes when
+ * you enter icons-only mode, and set it back to this when going to any other mode)
+ */
+ private boolean getSavedAutoCloseMode() {
+ return !AdtPrefs.getPrefs().getPaletteModes().contains(VALUE_NO_AUTOCLOSE);
+ }
+
+ /** Saves UI mode to persistent store to preserve palette mode across IDE sessions */
+ private void savePaletteMode() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mPaletteMode);
+ if (mAlphabetical) {
+ sb.append(',').append(VALUE_ALPHABETICAL);
+ }
+ if (!mCategories) {
+ sb.append(',').append(VALUE_NO_CATEGORIES);
+ }
+ if (!mAutoClose) {
+ sb.append(',').append(VALUE_NO_AUTOCLOSE);
+ }
+ AdtPrefs.getPrefs().setPaletteModes(sb.toString());
+ }
+
+ private void refreshPalette() {
+ IAndroidTarget oldTarget = mCurrentTarget;
+ mCurrentTarget = null;
+ mCurrentTargetData = null;
+ mCurrentTheme = null;
+ mCurrentDevice = null;
+ reloadPalette(oldTarget);
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+
+ @Override
+ public void dispose() {
+ if (mBackground != null) {
+ mBackground.dispose();
+ mBackground = null;
+ }
+ if (mForeground != null) {
+ mForeground.dispose();
+ mForeground = null;
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Returns the currently displayed target
+ *
+ * @return the current target, or null
+ */
+ public IAndroidTarget getCurrentTarget() {
+ return mCurrentTarget;
+ }
+
+ /**
+ * Returns the currently displayed theme (in palette modes that support previewing)
+ *
+ * @return the current theme, or null
+ */
+ public String getCurrentTheme() {
+ return mCurrentTheme;
+ }
+
+ /**
+ * Returns the currently displayed device (in palette modes that support previewing)
+ *
+ * @return the current device, or null
+ */
+ public String getCurrentDevice() {
+ return mCurrentDevice;
+ }
+
+ /** Returns true if previews in the palette should be made available */
+ private boolean previewsAvailable() {
+ // Not layoutlib 5 -- we require custom background support to do
+ // a decent job with previews
+ LayoutLibrary layoutLibrary = mEditor.getLayoutLibrary();
+ return layoutLibrary != null && layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR);
+ }
+
+ /**
+ * Loads or reloads the palette elements by using the layout and view descriptors from the
+ * given target data.
+ *
+ * @param target The target that has just been loaded
+ */
+ public void reloadPalette(IAndroidTarget target) {
+ ConfigurationChooser configChooser = mEditor.getConfigurationChooser();
+ String theme = configChooser.getThemeName();
+ String device = configChooser.getDeviceName();
+ if (device == null) {
+ return;
+ }
+ AndroidTargetData targetData =
+ target != null ? Sdk.getCurrent().getTargetData(target) : null;
+ if (target == mCurrentTarget && targetData == mCurrentTargetData
+ && mCurrentTheme != null && mCurrentTheme.equals(theme)
+ && mCurrentDevice != null && mCurrentDevice.equals(device)) {
+ return;
+ }
+ mCurrentTheme = theme;
+ mCurrentTarget = target;
+ mCurrentTargetData = targetData;
+ mCurrentDevice = device;
+ mPreviewIconFactory.reset();
+
+ if (targetData == null) {
+ return;
+ }
+
+ Set<String> expandedCategories = null;
+ if (mAccordion != null) {
+ expandedCategories = mAccordion.getExpandedCategories();
+ // We auto-expand all categories when showing icons-only. When returning to some
+ // other mode we don't want to retain all categories open.
+ if (expandedCategories.size() > 3) {
+ expandedCategories = null;
+ }
+ }
+
+ // Erase old content and recreate new
+ for (Control c : getChildren()) {
+ c.dispose();
+ }
+
+ if (mPaletteMode == null) {
+ loadPaletteMode();
+ assert mPaletteMode != null;
+ }
+
+ // Ensure that the palette mode is supported on this version of the layout library
+ if (!previewsAvailable()) {
+ if (mPaletteMode.isPreview()) {
+ mPaletteMode = PaletteMode.ICON_TEXT;
+ }
+ }
+
+ if (mPaletteMode.isPreview()) {
+ if (mForeground != null) {
+ mForeground.dispose();
+ mForeground = null;
+ }
+ if (mBackground != null) {
+ mBackground.dispose();
+ mBackground = null;
+ }
+ RGB background = mPreviewIconFactory.getBackgroundColor();
+ if (background != null) {
+ mBackground = new Color(getDisplay(), background);
+ }
+ RGB foreground = mPreviewIconFactory.getForegroundColor();
+ if (foreground != null) {
+ mForeground = new Color(getDisplay(), foreground);
+ }
+ }
+
+ List<String> headers = Collections.emptyList();
+ final Map<String, List<ViewElementDescriptor>> categoryToItems;
+ categoryToItems = new HashMap<String, List<ViewElementDescriptor>>();
+ headers = new ArrayList<String>();
+ List<Pair<String,List<ViewElementDescriptor>>> paletteEntries =
+ ViewMetadataRepository.get().getPaletteEntries(targetData,
+ mAlphabetical, mCategories);
+ for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) {
+ String category = pair.getFirst();
+ List<ViewElementDescriptor> categoryItems = pair.getSecond();
+ headers.add(category);
+ categoryToItems.put(category, categoryItems);
+ }
+
+ headers.add("Custom & Library Views");
+
+ // Set the categories to expand the first item if
+ // (1) we don't have a previously selected category, or
+ // (2) there's just one category anyway, or
+ // (3) the set of categories have changed so our previously selected category
+ // doesn't exist anymore (can happen when you toggle "Show Categories")
+ if ((expandedCategories == null && headers.size() > 0) || headers.size() == 1 ||
+ (expandedCategories != null && expandedCategories.size() >= 1
+ && !headers.contains(
+ expandedCategories.iterator().next().replace("&&", "&")))) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Expand the first category if we don't have a previous selection (e.g. refresh)
+ expandedCategories = Collections.singleton(headers.get(0));
+ }
+
+ boolean wrap = mPaletteMode.getWrap();
+
+ // Pack icon-only view vertically; others stretch to fill palette region
+ boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY;
+
+ mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap,
+ expandedCategories) {
+ @Override
+ protected Composite createChildContainer(Composite parent, Object header, int style) {
+ assert categoryToItems != null;
+ List<ViewElementDescriptor> list = categoryToItems.get(header);
+ final Composite composite;
+ if (list == null) {
+ assert header.equals("Custom & Library Views");
+
+ Composite wrapper = new Composite(parent, SWT.NONE);
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.marginWidth = gridLayout.marginHeight = 0;
+ gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
+ gridLayout.marginBottom = 3;
+ wrapper.setLayout(gridLayout);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ wrapper.setBackground(mBackground);
+ }
+ composite = super.createChildContainer(wrapper, header, style);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ composite.setBackground(mBackground);
+ }
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+ Button refreshButton = new Button(wrapper, SWT.PUSH | SWT.FLAT);
+ refreshButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
+ false, false, 1, 1));
+ refreshButton.setText("Refresh");
+ refreshButton.setImage(IconFactory.getInstance().getIcon("refresh")); //$NON-NLS-1$
+ refreshButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
+ finder.refresh(new ViewFinderListener(composite));
+ }
+ });
+
+ wrapper.layout(true);
+ } else {
+ composite = super.createChildContainer(parent, header, style);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ composite.setBackground(mBackground);
+ }
+ }
+ addMenu(composite);
+ return composite;
+ }
+ @Override
+ protected void createChildren(Composite parent, Object header) {
+ assert categoryToItems != null;
+ List<ViewElementDescriptor> list = categoryToItems.get(header);
+ if (list == null) {
+ assert header.equals("Custom & Library Views");
+ addCustomItems(parent);
+ return;
+ } else {
+ for (ViewElementDescriptor desc : list) {
+ createItem(parent, desc);
+ }
+ }
+ }
+ };
+ addMenu(mAccordion);
+ for (CLabel headerLabel : mAccordion.getHeaderLabels()) {
+ addMenu(headerLabel);
+ }
+ setLayout(new FillLayout());
+
+ // Expand All for icon-only mode, but don't store it as the persistent auto-close mode;
+ // when we enter other modes it will read back whatever persistent mode.
+ if (mPaletteMode == PaletteMode.ICON_ONLY) {
+ mAccordion.expandAll(true);
+ mAccordion.setAutoClose(false);
+ } else {
+ mAccordion.setAutoClose(getSavedAutoCloseMode());
+ }
+
+ layout(true);
+ }
+
+ protected void addCustomItems(final Composite parent) {
+ final CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
+ Collection<String> allViews = finder.getAllViews();
+ if (allViews == null) { // Not yet initialized: trigger an async refresh
+ finder.refresh(new ViewFinderListener(parent));
+ return;
+ }
+
+ // Remove previous content
+ for (Control c : parent.getChildren()) {
+ c.dispose();
+ }
+
+ // Add new views
+ for (final String fqcn : allViews) {
+ CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
+ ViewElementDescriptor desc = service.getDescriptor(mEditor.getProject(), fqcn);
+ if (desc == null) {
+ // The descriptor lookup performs validation steps of the class, and may
+ // in some cases determine that this is not a view and will return null;
+ // guard against that.
+ continue;
+ }
+
+ Control item = createItem(parent, desc);
+
+ // Add control-click listener on custom view items to you can warp to
+ // (and double click listener too -- the more discoverable, the better.)
+ if (item instanceof IconTextItem) {
+ IconTextItem it = (IconTextItem) item;
+ it.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ if ((e.stateMask & SWT.MOD1) != 0) {
+ AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /* package */ GraphicalEditorPart getEditor() {
+ return mEditor;
+ }
+
+ private Control createItem(Composite parent, ViewElementDescriptor desc) {
+ Control item = null;
+ switch (mPaletteMode) {
+ case SMALL_PREVIEW:
+ case TINY_PREVIEW:
+ case PREVIEW: {
+ ImageDescriptor descriptor = mPreviewIconFactory.getImageDescriptor(desc);
+ if (descriptor != null) {
+ Image image = descriptor.createImage();
+ ImageControl imageControl = new ImageControl(parent, SWT.None, image);
+ if (mPaletteMode.isScaledPreview()) {
+ // Try to preserve the overall size since rendering sizes typically
+ // vary with the dpi - so while the scaling factor for a 160 dpi
+ // rendering the scaling factor should be 0.5, for a 320 dpi one the
+ // scaling factor should be half that, 0.25.
+ float scale = 1.0f;
+ if (mPaletteMode == PaletteMode.SMALL_PREVIEW) {
+ scale = 0.75f;
+ } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) {
+ scale = 0.5f;
+ }
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ int dpi = chooser.getConfiguration().getDensity().getDpiValue();
+ while (dpi > 160) {
+ scale = scale / 2;
+ dpi = dpi / 2;
+ }
+ imageControl.setScale(scale);
+ }
+ imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE));
+ if (mBackground != null) {
+ imageControl.setBackground(mBackground);
+ }
+ String toolTip = desc.getUiName();
+ // It appears pretty much none of the descriptors have tooltips
+ //String descToolTip = desc.getTooltip();
+ //if (descToolTip != null && descToolTip.length() > 0) {
+ // toolTip = toolTip + "\n" + descToolTip;
+ //}
+ imageControl.setToolTipText(toolTip);
+
+ item = imageControl;
+ } else {
+ // Just use an Icon+Text item for these for now
+ item = new IconTextItem(parent, desc);
+ if (mForeground != null) {
+ item.setForeground(mForeground);
+ item.setBackground(mBackground);
+ }
+ }
+ break;
+ }
+ case ICON_TEXT: {
+ item = new IconTextItem(parent, desc);
+ break;
+ }
+ case ICON_ONLY: {
+ item = new ImageControl(parent, SWT.None, desc.getGenericIcon());
+ item.setToolTipText(desc.getUiName());
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("Not yet implemented");
+ }
+
+ final DragSource source = new DragSource(item, DND.DROP_COPY);
+ source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
+ source.addDragListener(new DescDragSourceListener(desc));
+ item.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ source.dispose();
+ }
+ });
+ addMenu(item);
+
+ return item;
+ }
+
+ /**
+ * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
+ * GLE2 canvas using drag'n'drop.
+ */
+ private static class IconTextItem extends CLabel implements MouseTrackListener {
+
+ private boolean mMouseIn;
+
+ public IconTextItem(Composite parent, ViewElementDescriptor desc) {
+ super(parent, SWT.NONE);
+ mMouseIn = false;
+
+ setText(desc.getUiName());
+ setImage(desc.getGenericIcon());
+ setToolTipText(desc.getTooltip());
+ addMouseTrackListener(this);
+ }
+
+ @Override
+ public int getStyle() {
+ int style = super.getStyle();
+ if (mMouseIn) {
+ style |= SWT.SHADOW_IN;
+ }
+ return style;
+ }
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ if (!mMouseIn) {
+ mMouseIn = true;
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ if (mMouseIn) {
+ mMouseIn = false;
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ // pass
+ }
+ }
+
+ /**
+ * A {@link DragSourceListener} that deals with drag'n'drop of
+ * {@link ElementDescriptor}s.
+ */
+ private class DescDragSourceListener implements DragSourceListener {
+ private final ViewElementDescriptor mDesc;
+ private SimpleElement[] mElements;
+
+ public DescDragSourceListener(ViewElementDescriptor desc) {
+ mDesc = desc;
+ }
+
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ // See if we can find out the bounds of this element from a preview image.
+ // Preview images are created before the drag source listener is notified
+ // of the started drag.
+ Rect bounds = null;
+ Rect dragBounds = null;
+
+ createDragImage(e);
+ if (mImage != null && !mIsPlaceholder) {
+ int width = mImageLayoutBounds.width;
+ int height = mImageLayoutBounds.height;
+ assert mImageLayoutBounds.x == 0;
+ assert mImageLayoutBounds.y == 0;
+ bounds = new Rect(0, 0, width, height);
+ double scale = mEditor.getCanvasControl().getScale();
+ int scaledWidth = (int) (scale * width);
+ int scaledHeight = (int) (scale * height);
+ int x = -scaledWidth / 2;
+ int y = -scaledHeight / 2;
+ dragBounds = new Rect(x, y, scaledWidth, scaledHeight);
+ }
+
+ SimpleElement se = new SimpleElement(
+ SimpleXmlTransfer.getFqcn(mDesc),
+ null /* parentFqcn */,
+ bounds /* bounds */,
+ null /* parentBounds */);
+ if (mDesc instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
+ pm.initializeNew(se);
+ }
+ mElements = new SimpleElement[] { se };
+
+ // Register this as the current dragged data
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ dragInfo.startDrag(
+ mElements,
+ null /* selection */,
+ null /* canvas */,
+ null /* removeSource */);
+ dragInfo.setDragBounds(dragBounds);
+ dragInfo.setDragBaseline(mBaseline);
+
+
+ e.doit = true;
+ }
+
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ // Provide the data for the drop when requested by the other side.
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mElements;
+ }
+ }
+
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Unregister the dragged data.
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+ mElements = null;
+ if (mImage != null) {
+ mImage.dispose();
+ mImage = null;
+ }
+ }
+
+ // TODO: Figure out the right dimensions to use for rendering.
+ // We WILL crop this after rendering, but for performance reasons it would be good
+ // not to make it much larger than necessary since to crop this we rely on
+ // actually scanning pixels.
+
+ /**
+ * Width of the rendered preview image (before it is cropped), although the actual
+ * width may be smaller (since we also take the device screen's size into account)
+ */
+ private static final int MAX_RENDER_HEIGHT = 400;
+
+ /**
+ * Height of the rendered preview image (before it is cropped), although the
+ * actual width may be smaller (since we also take the device screen's size into
+ * account)
+ */
+ private static final int MAX_RENDER_WIDTH = 500;
+
+ /** Amount of alpha to multiply into the image (divided by 256) */
+ private static final int IMG_ALPHA = 128;
+
+ /** The image shown during the drag */
+ private Image mImage;
+ /** The non-effect bounds of the drag image */
+ private Rectangle mImageLayoutBounds;
+ private int mBaseline = -1;
+
+ /**
+ * If true, the image is a preview of the view, and if not it is a "fallback"
+ * image of some sort, such as a rendering of the palette item itself
+ */
+ private boolean mIsPlaceholder;
+
+ private void createDragImage(DragSourceEvent event) {
+ mBaseline = -1;
+ Pair<Image, Rectangle> preview = renderPreview();
+ if (preview != null) {
+ mImage = preview.getFirst();
+ mImageLayoutBounds = preview.getSecond();
+ } else {
+ mImage = null;
+ mImageLayoutBounds = null;
+ }
+
+ mIsPlaceholder = mImage == null;
+ if (mIsPlaceholder) {
+ // Couldn't render preview (or the preview is a blank image, such as for
+ // example the preview of an empty layout), so instead create a placeholder
+ // image
+ // Render the palette item itself as an image
+ Control control = ((DragSource) event.widget).getControl();
+ GC gc = new GC(control);
+ Point size = control.getSize();
+ Display display = getDisplay();
+ final Image image = new Image(display, size.x, size.y);
+ gc.copyArea(image, 0, 0);
+ gc.dispose();
+
+ BufferedImage awtImage = SwtUtils.convertToAwt(image);
+ if (awtImage != null) {
+ awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */,
+ 0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */);
+ mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA);
+ } else {
+ ImageData data = image.getImageData();
+ data.alpha = IMG_ALPHA;
+
+ // Changing the ImageData -after- constructing an image on it
+ // has no effect, so we have to construct a new image. Luckily these
+ // are tiny images.
+ mImage = new Image(display, data);
+ }
+ image.dispose();
+ }
+
+ event.image = mImage;
+
+ if (!mIsPlaceholder) {
+ // Shift the drag feedback image up such that it's centered under the
+ // mouse pointer
+ double scale = mEditor.getCanvasControl().getScale();
+ event.offsetX = (int) (scale * mImageLayoutBounds.width / 2);
+ event.offsetY = (int) (scale * mImageLayoutBounds.height / 2);
+ }
+ }
+
+ /**
+ * Performs the actual rendering of the descriptor into an image and returns the
+ * image as well as the layout bounds of the image (not including drop shadow etc)
+ */
+ private Pair<Image, Rectangle> renderPreview() {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ RenderMode renderMode = repository.getRenderMode(mDesc.getFullClassName());
+ if (renderMode == RenderMode.SKIP) {
+ return null;
+ }
+
+ // Create blank XML document
+ Document document = DomUtilities.createEmptyDocument();
+
+ // Insert our target view's XML into it as a node
+ GraphicalEditorPart editor = getEditor();
+ LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
+
+ String viewName = mDesc.getXmlLocalName();
+ Element element = document.createElement(viewName);
+
+ // Set up a proper name space
+ Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID);
+ attr.setValue(ANDROID_URI);
+ element.getAttributes().setNamedItemNS(attr);
+
+ element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
+ element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
+
+ // This doesn't apply to all, but doesn't seem to cause harm and makes for a
+ // better experience with text-oriented views like buttons and texts
+ element.setAttributeNS(ANDROID_URI, ATTR_TEXT,
+ DescriptorsUtils.getBasename(mDesc.getUiName()));
+
+ // Is this a palette variation?
+ if (mDesc instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
+ pm.initializeNew(element);
+ }
+
+ document.appendChild(element);
+
+ // Construct UI model from XML
+ AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData();
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(layoutEditorDelegate.getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+ model.loadFromXmlNode(document);
+
+ // Call the create-hooks such that we for example insert mandatory
+ // children into views like the DialerFilter, apply image source attributes
+ // to ImageButtons, etc.
+ LayoutCanvas canvas = editor.getCanvasControl();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ UiElementNode parent = model.getUiRoot();
+ UiElementNode child = parent.getUiChildren().get(0);
+ if (child instanceof UiViewElementNode) {
+ UiViewElementNode childUiNode = (UiViewElementNode) child;
+ NodeProxy childNode = nodeFactory.create(childUiNode);
+
+ // Applying create hooks as part of palette render should
+ // not trigger model updates
+ layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(true);
+ try {
+ canvas.getRulesEngine().callCreateHooks(layoutEditorDelegate.getEditor(),
+ null, childNode, InsertType.CREATE_PREVIEW);
+ childNode.applyPendingChanges();
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Failed calling creation hooks for widget %1$s", viewName);
+ } finally {
+ layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(false);
+ }
+ }
+
+ Integer overrideBgColor = null;
+ boolean hasTransparency = false;
+ LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
+ if (layoutLibrary != null &&
+ layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
+ // It doesn't matter what the background color is as long as the alpha
+ // is 0 (fully transparent). We're using red to make it more obvious if
+ // for some reason the background is painted when it shouldn't be.
+ overrideBgColor = new Integer(0x00FF0000);
+ }
+
+ RenderSession session = null;
+ try {
+ // Use at most the size of the screen for the preview render.
+ // This is important since when we fill the size of certain views (like
+ // a SeekBar), we want it to at most be the width of the screen, and for small
+ // screens the RENDER_WIDTH was wider.
+ LayoutLog silentLogger = new LayoutLog();
+
+ session = RenderService.create(editor)
+ .setModel(model)
+ .setMaxRenderSize(MAX_RENDER_WIDTH, MAX_RENDER_HEIGHT)
+ .setLog(silentLogger)
+ .setOverrideBgColor(overrideBgColor)
+ .setDecorations(false)
+ .createRenderSession();
+ } catch (Throwable t) {
+ // Previews can fail for a variety of reasons -- let's not bug
+ // the user with it
+ return null;
+ }
+
+ if (session != null) {
+ if (session.getResult().isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null) {
+ BufferedImage cropped;
+ Rect initialCrop = null;
+ ViewInfo viewInfo = null;
+
+ List<ViewInfo> viewInfoList = session.getRootViews();
+
+ if (viewInfoList != null && viewInfoList.size() > 0) {
+ viewInfo = viewInfoList.get(0);
+ mBaseline = viewInfo.getBaseLine();
+ }
+
+ if (viewInfo != null) {
+ int x1 = viewInfo.getLeft();
+ int x2 = viewInfo.getRight();
+ int y2 = viewInfo.getBottom();
+ int y1 = viewInfo.getTop();
+ initialCrop = new Rect(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ if (hasTransparency) {
+ cropped = ImageUtils.cropBlank(image, initialCrop);
+ } else {
+ // Find out what the "background" color is such that we can properly
+ // crop it out of the image. To do this we pick out a pixel in the
+ // bottom right unpainted area. Rather than pick the one in the far
+ // bottom corner, we pick one as close to the bounds of the view as
+ // possible (but still outside of the bounds), such that we can
+ // deal with themes like the dialog theme.
+ int edgeX = image.getWidth() -1;
+ int edgeY = image.getHeight() -1;
+ if (viewInfo != null) {
+ if (viewInfo.getRight() < image.getWidth()-1) {
+ edgeX = viewInfo.getRight()+1;
+ }
+ if (viewInfo.getBottom() < image.getHeight()-1) {
+ edgeY = viewInfo.getBottom()+1;
+ }
+ }
+ int edgeColor = image.getRGB(edgeX, edgeY);
+ cropped = ImageUtils.cropColor(image, edgeColor, initialCrop);
+ }
+
+ if (cropped != null) {
+ int width = initialCrop != null ? initialCrop.w : cropped.getWidth();
+ int height = initialCrop != null ? initialCrop.h : cropped.getHeight();
+ boolean needsContrast = hasTransparency
+ && !ImageUtils.containsDarkPixels(cropped);
+ cropped = ImageUtils.createDropShadow(cropped,
+ hasTransparency ? 3 : 5 /* shadowSize */,
+ !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f/*alpha*/,
+ 0x000000 /* shadowRgb */);
+
+ double scale = canvas.getScale();
+ if (scale != 1L) {
+ cropped = ImageUtils.scale(cropped, scale, scale);
+ }
+
+ Display display = getDisplay();
+ int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1;
+ Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha);
+ Rectangle imageBounds = new Rectangle(0, 0, width, height);
+ return Pair.of(swtImage, imageBounds);
+ }
+ }
+ }
+
+ session.dispose();
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method to print out the contents of the given XML document. This is
+ * really useful when working on the preview code above. I'm including all the
+ * code inside a constant false, which means the compiler will omit all the code,
+ * but I'd like to leave it in the code base and by doing it this way rather than
+ * as commented out code the code won't be accidentally broken.
+ */
+ @SuppressWarnings("all")
+ private void dumpDocument(Document document) {
+ // Diagnostics: print out the XML that we're about to render
+ if (false) { // Will be omitted by the compiler
+ org.apache.xml.serialize.OutputFormat outputFormat =
+ new org.apache.xml.serialize.OutputFormat(
+ "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
+ outputFormat.setIndent(2);
+ outputFormat.setLineWidth(100);
+ outputFormat.setIndenting(true);
+ outputFormat.setOmitXMLDeclaration(true);
+ outputFormat.setOmitDocumentType(true);
+ StringWriter stringWriter = new StringWriter();
+ // Using FQN here to avoid having an import above, which will result
+ // in a deprecation warning, and there isn't a way to annotate a single
+ // import element with a SuppressWarnings.
+ org.apache.xml.serialize.XMLSerializer serializer =
+ new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
+ serializer.setNamespaces(true);
+ try {
+ serializer.serialize(document.getDocumentElement());
+ System.out.println(stringWriter.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /** Action for switching view modes via radio buttons */
+ private class PaletteModeAction extends Action {
+ private final PaletteMode mMode;
+
+ PaletteModeAction(PaletteMode mode) {
+ super(mode.getActionLabel(), IAction.AS_RADIO_BUTTON);
+ mMode = mode;
+ boolean selected = mMode == mPaletteMode;
+ setChecked(selected);
+ setEnabled(!selected);
+ }
+
+ @Override
+ public void run() {
+ if (isEnabled()) {
+ mPaletteMode = mMode;
+ refreshPalette();
+ savePaletteMode();
+ }
+ }
+ }
+
+ /** Action for toggling various checkbox view modes - categories, sorting, etc */
+ private class ToggleViewOptionAction extends Action {
+ private final int mAction;
+ final static int TOGGLE_CATEGORY = 1;
+ final static int TOGGLE_ALPHABETICAL = 2;
+ final static int TOGGLE_AUTO_CLOSE = 3;
+ final static int REFRESH = 4;
+ final static int RESET = 5;
+
+ ToggleViewOptionAction(String title, int action, boolean checked) {
+ super(title, (action == REFRESH || action == RESET) ? IAction.AS_PUSH_BUTTON
+ : IAction.AS_CHECK_BOX);
+ mAction = action;
+ if (checked) {
+ setChecked(checked);
+ }
+ }
+
+ @Override
+ public void run() {
+ switch (mAction) {
+ case TOGGLE_CATEGORY:
+ mCategories = !mCategories;
+ refreshPalette();
+ break;
+ case TOGGLE_ALPHABETICAL:
+ mAlphabetical = !mAlphabetical;
+ refreshPalette();
+ break;
+ case TOGGLE_AUTO_CLOSE:
+ mAutoClose = !mAutoClose;
+ mAccordion.setAutoClose(mAutoClose);
+ break;
+ case REFRESH:
+ mPreviewIconFactory.refresh();
+ refreshPalette();
+ break;
+ case RESET:
+ mAlphabetical = false;
+ mCategories = true;
+ mAutoClose = true;
+ mPaletteMode = PaletteMode.SMALL_PREVIEW;
+ refreshPalette();
+ break;
+ }
+ savePaletteMode();
+ }
+ }
+
+ private void addMenu(Control control) {
+ control.addMenuDetectListener(new MenuDetectListener() {
+ @Override
+ public void menuDetected(MenuDetectEvent e) {
+ showMenu(e.x, e.y);
+ }
+ });
+ }
+
+ private void showMenu(int x, int y) {
+ MenuManager manager = new MenuManager() {
+ @Override
+ public boolean isDynamic() {
+ return true;
+ }
+ };
+ boolean previews = previewsAvailable();
+ for (PaletteMode mode : PaletteMode.values()) {
+ if (mode.isPreview() && !previews) {
+ continue;
+ }
+ manager.add(new PaletteModeAction(mode));
+ }
+ if (mPaletteMode.isPreview()) {
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Refresh Previews",
+ ToggleViewOptionAction.REFRESH,
+ false));
+ }
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Show Categories",
+ ToggleViewOptionAction.TOGGLE_CATEGORY,
+ mCategories));
+ manager.add(new ToggleViewOptionAction("Sort Alphabetically",
+ ToggleViewOptionAction.TOGGLE_ALPHABETICAL,
+ mAlphabetical));
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Auto Close Previous",
+ ToggleViewOptionAction.TOGGLE_AUTO_CLOSE,
+ mAutoClose));
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Reset",
+ ToggleViewOptionAction.RESET,
+ false));
+
+ Menu menu = manager.createContextMenu(PaletteControl.this);
+ menu.setLocation(x, y);
+ menu.setVisible(true);
+ }
+
+ private final class ViewFinderListener implements CustomViewFinder.Listener {
+ private final Composite mParent;
+
+ private ViewFinderListener(Composite parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public void viewsUpdated(Collection<String> customViews,
+ Collection<String> thirdPartyViews) {
+ addCustomItems(mParent);
+ mParent.layout(true);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java
new file mode 100644
index 000000000..629a42f18
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_ANIMATOR;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.IAnimationListener;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchWindow;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * "Play Animation" context menu which lists available animations in the project and in
+ * the framework, as well as a "Create Animation" shortcut, and allows the animation to be
+ * run on the selection
+ * <p/>
+ * TODO: Add transport controls for play/rewind/pause/loop, and (if possible) scrubbing
+ */
+public class PlayAnimationMenu extends SubmenuAction {
+ /** Associated canvas */
+ private final LayoutCanvas mCanvas;
+ /** Whether this menu is showing local animations or framework animations */
+ private boolean mFramework;
+
+ /**
+ * Creates a "Play Animation" menu
+ *
+ * @param canvas associated canvas
+ */
+ public PlayAnimationMenu(LayoutCanvas canvas) {
+ this(canvas, "Play Animation", false);
+ }
+
+ /**
+ * Creates an animation menu; this can be used either for the outer Play animation
+ * menu, or the inner frameworks-animations list
+ *
+ * @param canvas the associated canvas
+ * @param title menu item name
+ * @param framework true to show the framework animations, false for the project (and
+ * nested framework-animation-menu) animations
+ */
+ private PlayAnimationMenu(LayoutCanvas canvas, String title, boolean framework) {
+ super(title);
+ mCanvas = canvas;
+ mFramework = framework;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ List<SelectionItem> selection = selectionManager.getSelections();
+ if (selection.size() != 1) {
+ addDisabledMessageItem("Select exactly one widget");
+ return;
+ }
+
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (graphicalEditor.renderingSupports(Capability.PLAY_ANIMATION)) {
+ // List of animations
+ Collection<String> animationNames = graphicalEditor.getResourceNames(mFramework,
+ ResourceType.ANIMATOR);
+ if (animationNames.size() > 0) {
+ // Sort alphabetically
+ List<String> sortedNames = new ArrayList<String>(animationNames);
+ Collections.sort(sortedNames);
+
+ for (String animation : sortedNames) {
+ String title = animation;
+ IAction action = new PlayAnimationAction(title, animation, mFramework);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+
+ new Separator().fill(menu, -1);
+ }
+
+ if (!mFramework) {
+ // Not in the framework submenu: include recent list and create new actions
+
+ // "Create New" action
+ new ActionContributionItem(new CreateAnimationAction()).fill(menu, -1);
+
+ // Framework resources submenu
+ new Separator().fill(menu, -1);
+ PlayAnimationMenu sub = new PlayAnimationMenu(mCanvas, "Android Builtin", true);
+ new ActionContributionItem(sub).fill(menu, -1);
+ }
+ } else {
+ addDisabledMessageItem(
+ "Not supported for this SDK version; try changing the Render Target");
+ }
+ }
+
+ private class PlayAnimationAction extends Action {
+ private final String mAnimationName;
+ private final boolean mIsFrameworkAnim;
+
+ public PlayAnimationAction(String title, String animationName, boolean isFrameworkAnim) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mAnimationName = animationName;
+ mIsFrameworkAnim = isFrameworkAnim;
+ }
+
+ @Override
+ public void run() {
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ List<SelectionItem> selection = selectionManager.getSelections();
+ SelectionItem canvasSelection = selection.get(0);
+ CanvasViewInfo info = canvasSelection.getViewInfo();
+
+ Object viewObject = info.getViewObject();
+ if (viewObject != null) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ RenderSession session = viewHierarchy.getSession();
+ Result r = session.animate(viewObject, mAnimationName, mIsFrameworkAnim,
+ new IAnimationListener() {
+ private boolean mPendingDrawing = false;
+
+ @Override
+ public void onNewFrame(RenderSession s) {
+ SelectionOverlay selectionOverlay = mCanvas.getSelectionOverlay();
+ if (!selectionOverlay.isHiding()) {
+ selectionOverlay.setHiding(true);
+ }
+ HoverOverlay hoverOverlay = mCanvas.getHoverOverlay();
+ if (!hoverOverlay.isHiding()) {
+ hoverOverlay.setHiding(true);
+ }
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ imageOverlay.setImage(s.getImage(), s.isAlphaChannelImage());
+ synchronized (this) {
+ if (mPendingDrawing == false) {
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (this) {
+ mPendingDrawing = false;
+ }
+ mCanvas.redraw();
+ }
+ });
+ mPendingDrawing = true;
+ }
+ }
+ }
+
+ @Override
+ public boolean isCanceled() {
+ return false;
+ }
+
+ @Override
+ public void done(Result result) {
+ SelectionOverlay selectionOverlay = mCanvas.getSelectionOverlay();
+ selectionOverlay.setHiding(false);
+ HoverOverlay hoverOverlay = mCanvas.getHoverOverlay();
+ hoverOverlay.setHiding(false);
+
+ // Must refresh view hierarchy to force objects back to
+ // their original positions in case animations have left
+ // them elsewhere
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ GraphicalEditorPart graphicalEditor = mCanvas
+ .getEditorDelegate().getGraphicalEditor();
+ graphicalEditor.recomputeLayout();
+ }
+ });
+ }
+ });
+
+ if (!r.isSuccess()) {
+ if (r.getErrorMessage() != null) {
+ AdtPlugin.log(r.getException(), r.getErrorMessage());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Action which brings up the "Create new XML File" wizard, pre-selected with the
+ * animation category
+ */
+ private class CreateAnimationAction extends Action {
+ public CreateAnimationAction() {
+ super("Create...", IAction.AS_PUSH_BUTTON);
+ }
+
+ @Override
+ public void run() {
+ Shell parent = mCanvas.getShell();
+ NewXmlFileWizard wizard = new NewXmlFileWizard();
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ IWorkbenchWindow workbenchWindow =
+ editor.getEditor().getEditorSite().getWorkbenchWindow();
+ IWorkbench workbench = workbenchWindow.getWorkbench();
+ String animationDir = FD_RESOURCES + WS_SEP + FD_RES_ANIMATOR;
+ Pair<IProject, String> pair = Pair.of(editor.getEditor().getProject(), animationDir);
+ IStructuredSelection selection = new StructuredSelection(pair);
+ wizard.init(workbench, selection);
+ WizardDialog dialog = new WizardDialog(parent, wizard);
+ dialog.create();
+ dialog.open();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java
new file mode 100644
index 000000000..5661b2919
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.DOT_PNG;
+import static com.android.SdkConstants.FQCN_DATE_PICKER;
+import static com.android.SdkConstants.FQCN_EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_TIME_PICKER;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.rendering.api.StyleResourceValue;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.RGB;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Factory which can provide preview icons for android views of a particular SDK and
+ * editor's configuration chooser
+ */
+public class PreviewIconFactory {
+ private PaletteControl mPalette;
+ private RGB mBackground;
+ private RGB mForeground;
+ private File mImageDir;
+
+ private static final String PREVIEW_INFO_FILE = "preview.properties"; //$NON-NLS-1$
+
+ public PreviewIconFactory(PaletteControl palette) {
+ mPalette = palette;
+ }
+
+ /**
+ * Resets the state in the preview icon factory such that it will re-fetch information
+ * like the theme and SDK (the icons themselves are cached in a directory across IDE
+ * session though)
+ */
+ public void reset() {
+ mImageDir = null;
+ mBackground = null;
+ mForeground = null;
+ }
+
+ /**
+ * Deletes all the persistent state for the current settings such that it will be regenerated
+ */
+ public void refresh() {
+ File imageDir = getImageDir(false);
+ if (imageDir != null && imageDir.exists()) {
+ File[] files = imageDir.listFiles();
+ for (File file : files) {
+ file.delete();
+ }
+ imageDir.delete();
+ reset();
+ }
+ }
+
+ /**
+ * Returns an image descriptor for the given element descriptor, or null if no image
+ * could be computed. The rendering parameters (SDK, theme etc) correspond to those
+ * stored in the associated palette.
+ *
+ * @param desc the element descriptor to get an image for
+ * @return an image descriptor, or null if no image could be rendered
+ */
+ public ImageDescriptor getImageDescriptor(ElementDescriptor desc) {
+ File imageDir = getImageDir(false);
+ if (!imageDir.exists()) {
+ render();
+ }
+ File file = new File(imageDir, getFileName(desc));
+ if (file.exists()) {
+ try {
+ return ImageDescriptor.createFromURL(file.toURI().toURL());
+ } catch (MalformedURLException e) {
+ AdtPlugin.log(e, "Could not create image descriptor for %s", file);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Partition the elements in the document according to their rendering preferences;
+ * elements that should be skipped are removed, elements that should be rendered alone
+ * are placed in their own list, etc
+ *
+ * @param document the document containing render fragments for the various elements
+ * @return
+ */
+ private List<List<Element>> partitionRenderElements(Document document) {
+ List<List<Element>> elements = new ArrayList<List<Element>>();
+
+ List<Element> shared = new ArrayList<Element>();
+ Element root = document.getDocumentElement();
+ elements.add(shared);
+
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+
+ NodeList children = root.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String fqn = repository.getFullClassName(element);
+ assert fqn.length() > 0 : element.getNodeName();
+ RenderMode renderMode = repository.getRenderMode(fqn);
+
+ // Temporary special cases
+ if (fqn.equals(FQCN_LIST_VIEW) || fqn.equals(FQCN_EXPANDABLE_LIST_VIEW)) {
+ if (!mPalette.getEditor().renderingSupports(Capability.ADAPTER_BINDING)) {
+ renderMode = RenderMode.SKIP;
+ }
+ } else if (fqn.equals(FQCN_DATE_PICKER) || fqn.equals(FQCN_TIME_PICKER)) {
+ IAndroidTarget renderingTarget = mPalette.getEditor().getRenderingTarget();
+ // In Honeycomb, these widgets only render properly in the Holo themes.
+ int apiLevel = renderingTarget.getVersion().getApiLevel();
+ if (apiLevel == 11) {
+ String themeName = mPalette.getCurrentTheme();
+ if (themeName == null || !themeName.startsWith("Theme.Holo")) { //$NON-NLS-1$
+ // Note - it's possible that the the theme is some other theme
+ // such as a user theme which inherits from Theme.Holo and that
+ // the render -would- have worked, but it's harder to detect that
+ // scenario, so we err on the side of caution and just show an
+ // icon + name for the time widgets.
+ renderMode = RenderMode.SKIP;
+ }
+ } else if (apiLevel >= 12) {
+ // Currently broken, even for Holo.
+ renderMode = RenderMode.SKIP;
+ } // apiLevel <= 10 is fine
+ }
+
+ if (renderMode == RenderMode.ALONE) {
+ elements.add(Collections.singletonList(element));
+ } else if (renderMode == RenderMode.NORMAL) {
+ shared.add(element);
+ } else {
+ assert renderMode == RenderMode.SKIP;
+ }
+ }
+ }
+
+ return elements;
+ }
+
+ /**
+ * Renders ALL the widgets and then extracts image data for each view and saves it on
+ * disk
+ */
+ private boolean render() {
+ File imageDir = getImageDir(true);
+
+ GraphicalEditorPart editor = mPalette.getEditor();
+ LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
+ LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
+ Integer overrideBgColor = null;
+ if (layoutLibrary != null) {
+ if (layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
+ Pair<RGB, RGB> themeColors = getColorsFromTheme();
+ RGB bg = themeColors.getFirst();
+ RGB fg = themeColors.getSecond();
+ if (bg != null) {
+ storeBackground(imageDir, bg, fg);
+ overrideBgColor = Integer.valueOf(ImageUtils.rgbToInt(bg, 0xFF));
+ }
+ }
+ }
+
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ Document document = repository.getRenderingConfigDoc();
+
+ if (document == null) {
+ return false;
+ }
+
+ // Construct UI model from XML
+ AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData();
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(layoutEditorDelegate.getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+
+ Element documentElement = document.getDocumentElement();
+ List<List<Element>> elements = partitionRenderElements(document);
+ for (List<Element> elementGroup : elements) {
+ // Replace the document elements with the current element group
+ while (documentElement.getFirstChild() != null) {
+ documentElement.removeChild(documentElement.getFirstChild());
+ }
+ for (Element element : elementGroup) {
+ documentElement.appendChild(element);
+ }
+
+ model.loadFromXmlNode(document);
+
+ RenderSession session = null;
+ NodeList childNodes = documentElement.getChildNodes();
+ try {
+ // Important to get these sizes large enough for clients that don't support
+ // RenderMode.FULL_EXPAND such as 1.6
+ int width = 200;
+ int height = childNodes.getLength() == 1 ? 400 : 1600;
+
+ session = RenderService.create(editor)
+ .setModel(model)
+ .setOverrideRenderSize(width, height)
+ .setRenderingMode(RenderingMode.FULL_EXPAND)
+ .setLog(editor.createRenderLogger("palette"))
+ .setOverrideBgColor(overrideBgColor)
+ .setDecorations(false)
+ .createRenderSession();
+ } catch (Throwable t) {
+ // If there are internal errors previewing the components just revert to plain
+ // icons and labels
+ continue;
+ }
+
+ if (session != null) {
+ if (session.getResult().isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
+
+ // Fallback for older platforms where we couldn't do background rendering
+ // at the beginning of this method
+ if (mBackground == null) {
+ Pair<RGB, RGB> themeColors = getColorsFromTheme();
+ RGB bg = themeColors.getFirst();
+ RGB fg = themeColors.getSecond();
+
+ if (bg == null) {
+ // Just use a pixel from the rendering instead.
+ int p = image.getRGB(image.getWidth() - 1, image.getHeight() - 1);
+ // However, in this case we don't trust the foreground color
+ // even if one was found in the themes; pick one that is guaranteed
+ // to contrast with the background
+ bg = ImageUtils.intToRgb(p);
+ if (ImageUtils.getBrightness(ImageUtils.rgbToInt(bg, 255)) < 128) {
+ fg = new RGB(255, 255, 255);
+ } else {
+ fg = new RGB(0, 0, 0);
+ }
+ }
+ storeBackground(imageDir, bg, fg);
+ assert mBackground != null;
+ }
+
+ List<ViewInfo> viewInfoList = session.getRootViews();
+ if (viewInfoList != null && viewInfoList.size() > 0) {
+ // We don't render previews under a <merge> so there should
+ // only be one root.
+ ViewInfo firstRoot = viewInfoList.get(0);
+ int parentX = firstRoot.getLeft();
+ int parentY = firstRoot.getTop();
+ List<ViewInfo> infos = firstRoot.getChildren();
+ for (ViewInfo info : infos) {
+ Object cookie = info.getCookie();
+ if (!(cookie instanceof UiElementNode)) {
+ continue;
+ }
+ UiElementNode node = (UiElementNode) cookie;
+ String fileName = getFileName(node);
+ File file = new File(imageDir, fileName);
+ if (file.exists()) {
+ // On Windows, perhaps we need to rename instead?
+ file.delete();
+ }
+ int x1 = parentX + info.getLeft();
+ int y1 = parentY + info.getTop();
+ int x2 = parentX + info.getRight();
+ int y2 = parentY + info.getBottom();
+ if (x1 != x2 && y1 != y2) {
+ savePreview(file, image, x1, y1, x2, y2);
+ }
+ }
+ }
+ }
+ } else {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ Node node = childNodes.item(i);
+ if (node instanceof Element) {
+ Element e = (Element) node;
+ String fqn = repository.getFullClassName(e);
+ fqn = fqn.substring(fqn.lastIndexOf('.') + 1);
+ if (sb.length() > 0) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ sb.append(fqn);
+ }
+ }
+ AdtPlugin.log(IStatus.WARNING, "Failed to render set of icons for %1$s",
+ sb.toString());
+
+ if (session.getResult().getException() != null) {
+ AdtPlugin.log(session.getResult().getException(),
+ session.getResult().getErrorMessage());
+ } else if (session.getResult().getErrorMessage() != null) {
+ AdtPlugin.log(IStatus.WARNING, session.getResult().getErrorMessage());
+ }
+ }
+
+ session.dispose();
+ }
+ }
+
+ mPalette.getEditor().recomputeLayout();
+
+ return true;
+ }
+
+ /**
+ * Look up the background and foreground colors from the theme. May not find either
+ * the background or foreground or both, but will always return a pair of possibly
+ * null colors.
+ *
+ * @return a pair of possibly null color descriptions
+ */
+ @NonNull
+ private Pair<RGB, RGB> getColorsFromTheme() {
+ RGB background = null;
+ RGB foreground = null;
+
+ ResourceResolver resources = mPalette.getEditor().getResourceResolver();
+ if (resources == null) {
+ return Pair.of(background, foreground);
+ }
+ StyleResourceValue theme = resources.getCurrentTheme();
+ if (theme != null) {
+ background = resolveThemeColor(resources, "windowBackground"); //$NON-NLS-1$
+ if (background == null) {
+ background = renderDrawableResource("windowBackground"); //$NON-NLS-1$
+ // This causes some harm with some themes: We'll find a color, say black,
+ // that isn't actually rendered in the theme. Better to use null here,
+ // which will cause the caller to pick a pixel from the observed background
+ // instead.
+ //if (background == null) {
+ // background = resolveThemeColor(resources, "colorBackground"); //$NON-NLS-1$
+ //}
+ }
+ foreground = resolveThemeColor(resources, "textColorPrimary"); //$NON-NLS-1$
+ }
+
+ // Ensure that the foreground color is suitably distinct from the background color
+ if (background != null) {
+ int bgRgb = ImageUtils.rgbToInt(background, 0xFF);
+ int backgroundBrightness = ImageUtils.getBrightness(bgRgb);
+ if (foreground == null) {
+ if (backgroundBrightness < 128) {
+ foreground = new RGB(255, 255, 255);
+ } else {
+ foreground = new RGB(0, 0, 0);
+ }
+ } else {
+ int fgRgb = ImageUtils.rgbToInt(foreground, 0xFF);
+ int foregroundBrightness = ImageUtils.getBrightness(fgRgb);
+ if (Math.abs(backgroundBrightness - foregroundBrightness) < 64) {
+ if (backgroundBrightness < 128) {
+ foreground = new RGB(255, 255, 255);
+ } else {
+ foreground = new RGB(0, 0, 0);
+ }
+ }
+ }
+ }
+
+ return Pair.of(background, foreground);
+ }
+
+ /**
+ * Renders the given resource which should refer to a drawable and returns a
+ * representative color value for the drawable (such as the color in the center)
+ *
+ * @param themeItemName the item in the theme to be looked up and rendered
+ * @return a color representing a typical color in the drawable
+ */
+ private RGB renderDrawableResource(String themeItemName) {
+ GraphicalEditorPart editor = mPalette.getEditor();
+ ResourceResolver resources = editor.getResourceResolver();
+ ResourceValue resourceValue = resources.findItemInTheme(themeItemName);
+ BufferedImage image = RenderService.create(editor)
+ .setOverrideRenderSize(100, 100)
+ .renderDrawable(resourceValue);
+ if (image != null) {
+ // Use the middle pixel as the color since that works better for gradients;
+ // solid colors work too.
+ int rgb = image.getRGB(image.getWidth() / 2, image.getHeight() / 2);
+ return ImageUtils.intToRgb(rgb);
+ }
+
+ return null;
+ }
+
+ private static RGB resolveThemeColor(ResourceResolver resources, String resourceName) {
+ ResourceValue textColor = resources.findItemInTheme(resourceName);
+ return ResourceHelper.resolveColor(resources, textColor);
+ }
+
+ private String getFileName(ElementDescriptor descriptor) {
+ if (descriptor instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pmd = (PaletteMetadataDescriptor) descriptor;
+ StringBuilder sb = new StringBuilder();
+ String name = pmd.getUiName();
+ // Strip out whitespace, parentheses, etc.
+ for (int i = 0, n = name.length(); i < n; i++) {
+ char c = name.charAt(i);
+ if (Character.isLetter(c)) {
+ sb.append(c);
+ }
+ }
+ return sb.toString() + DOT_PNG;
+ }
+ return descriptor.getUiName() + DOT_PNG;
+ }
+
+ private String getFileName(UiElementNode node) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ String fqn = repository.getFullClassName((Element) node.getXmlNode());
+ return fqn.substring(fqn.lastIndexOf('.') + 1) + DOT_PNG;
+ }
+
+ /**
+ * Cleans up a name by removing punctuation and whitespace etc to make
+ * it a better filename
+ * @param name the name to clean
+ * @return a cleaned up name
+ */
+ @NonNull
+ private static String cleanup(@Nullable String name) {
+ if (name == null) {
+ return "";
+ }
+
+ // Extract just the characters (no whitespace, parentheses, punctuation etc)
+ // to ensure that the filename is pretty portable
+ StringBuilder sb = new StringBuilder(name.length());
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (Character.isJavaIdentifierPart(c)) {
+ sb.append(Character.toLowerCase(c));
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /** Returns the location of a directory containing image previews (which may not exist) */
+ private File getImageDir(boolean create) {
+ if (mImageDir == null) {
+ // Location for plugin-related state data
+ IPath pluginState = AdtPlugin.getDefault().getStateLocation();
+
+ // We have multiple directories - one for each combination of SDK, theme and device
+ // (and later, possibly other qualifiers).
+ // These are created -lazily-.
+ String targetName = mPalette.getCurrentTarget().hashString();
+ String androidTargetNamePrefix = "android-";
+ String themeNamePrefix = "Theme.";
+ if (targetName.startsWith(androidTargetNamePrefix)) {
+ targetName = targetName.substring(androidTargetNamePrefix.length());
+ }
+ String themeName = mPalette.getCurrentTheme();
+ if (themeName == null) {
+ themeName = "Theme"; //$NON-NLS-1$
+ }
+ if (themeName.startsWith(themeNamePrefix)) {
+ themeName = themeName.substring(themeNamePrefix.length());
+ }
+ targetName = cleanup(targetName);
+ themeName = cleanup(themeName);
+ String deviceName = cleanup(mPalette.getCurrentDevice());
+ String dirName = String.format("palette-preview-r16b-%s-%s-%s", targetName,
+ themeName, deviceName);
+ IPath dirPath = pluginState.append(dirName);
+
+ mImageDir = new File(dirPath.toOSString());
+ }
+
+ if (create && !mImageDir.exists()) {
+ mImageDir.mkdirs();
+ }
+
+ return mImageDir;
+ }
+
+ private void savePreview(File output, BufferedImage image,
+ int left, int top, int right, int bottom) {
+ try {
+ BufferedImage im = ImageUtils.subImage(image, left, top, right, bottom);
+ ImageIO.write(im, "PNG", output); //$NON-NLS-1$
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed writing palette file");
+ }
+ }
+
+ private void storeBackground(File imageDir, RGB bg, RGB fg) {
+ mBackground = bg;
+ mForeground = fg;
+ File file = new File(imageDir, PREVIEW_INFO_FILE);
+ String colors = String.format(
+ "background=#%02x%02x%02x\nforeground=#%02x%02x%02x\n", //$NON-NLS-1$
+ bg.red, bg.green, bg.blue,
+ fg.red, fg.green, fg.blue);
+ AdtPlugin.writeFile(file, colors);
+ }
+
+ public RGB getBackgroundColor() {
+ if (mBackground == null) {
+ initColors();
+ }
+
+ return mBackground;
+ }
+
+ public RGB getForegroundColor() {
+ if (mForeground == null) {
+ initColors();
+ }
+
+ return mForeground;
+ }
+
+ public void initColors() {
+ try {
+ // Already initialized? Foreground can be null which would call
+ // initColors again and again, but background is never null after
+ // initialization so we use it as the have-initialized flag.
+ if (mBackground != null) {
+ return;
+ }
+
+ File imageDir = getImageDir(false);
+ if (!imageDir.exists()) {
+ render();
+
+ // Initialized as part of the render
+ if (mBackground != null) {
+ return;
+ }
+ }
+
+ File file = new File(imageDir, PREVIEW_INFO_FILE);
+ if (file.exists()) {
+ Properties properties = new Properties();
+ InputStream is = null;
+ try {
+ is = new BufferedInputStream(new FileInputStream(file));
+ properties.load(is);
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read preview properties");
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // Nothing useful can be done.
+ }
+ }
+ }
+
+ String colorString = (String) properties.get("background"); //$NON-NLS-1$
+ if (colorString != null) {
+ int rgb = ImageUtils.getColor(colorString.trim());
+ mBackground = ImageUtils.intToRgb(rgb);
+ }
+ colorString = (String) properties.get("foreground"); //$NON-NLS-1$
+ if (colorString != null) {
+ int rgb = ImageUtils.getColor(colorString.trim());
+ mForeground = ImageUtils.intToRgb(rgb);
+ }
+ }
+
+ if (mBackground == null) {
+ mBackground = new RGB(0, 0, 0);
+ }
+ // mForeground is allowed to be null.
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Cannot initialize preview color settings");
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java
new file mode 100644
index 000000000..8548830bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.runtime.IStatus;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A {@link LayoutLog} which records the problems it encounters and offers them as a
+ * single summary at the end
+ */
+public class RenderLogger extends LayoutLog {
+ static final String TAG_MISSING_DIMENSION = "missing.dimension"; //$NON-NLS-1$
+
+ private final String mName;
+ private List<String> mFidelityWarnings;
+ private List<String> mWarnings;
+ private List<String> mErrors;
+ private boolean mHaveExceptions;
+ private List<String> mTags;
+ private List<Throwable> mTraces;
+ private static Set<String> sIgnoredFidelityWarnings;
+ private final Object mCredential;
+
+ /** Construct a logger for the given named layout */
+ RenderLogger(String name, Object credential) {
+ mName = name;
+ mCredential = credential;
+ }
+
+ /**
+ * Are there any logged errors or warnings during the render?
+ *
+ * @return true if there were problems during the render
+ */
+ public boolean hasProblems() {
+ return mFidelityWarnings != null || mErrors != null || mWarnings != null ||
+ mHaveExceptions;
+ }
+
+ /**
+ * Returns a list of traces encountered during rendering, or null if none
+ *
+ * @return a list of traces encountered during rendering, or null if none
+ */
+ @Nullable
+ public List<Throwable> getFirstTrace() {
+ return mTraces;
+ }
+
+ /**
+ * Returns a (possibly multi-line) description of all the problems
+ *
+ * @param includeFidelityWarnings if true, include fidelity warnings in the problem
+ * summary
+ * @return a string describing the rendering problems
+ */
+ @NonNull
+ public String getProblems(boolean includeFidelityWarnings) {
+ StringBuilder sb = new StringBuilder();
+
+ if (mErrors != null) {
+ for (String error : mErrors) {
+ sb.append(error).append('\n');
+ }
+ }
+
+ if (mWarnings != null) {
+ for (String warning : mWarnings) {
+ sb.append(warning).append('\n');
+ }
+ }
+
+ if (includeFidelityWarnings && mFidelityWarnings != null) {
+ sb.append("The graphics preview in the layout editor may not be accurate:\n");
+ for (String warning : mFidelityWarnings) {
+ sb.append("* ");
+ sb.append(warning).append('\n');
+ }
+ }
+
+ if (mHaveExceptions) {
+ sb.append("Exception details are logged in Window > Show View > Error Log");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the fidelity warnings
+ *
+ * @return the fidelity warnings
+ */
+ @Nullable
+ public List<String> getFidelityWarnings() {
+ return mFidelityWarnings;
+ }
+
+ // ---- extends LayoutLog ----
+
+ @Override
+ public void error(String tag, String message, Object data) {
+ String description = describe(message);
+
+ appendToIdeLog(null, IStatus.ERROR, description);
+
+ // Workaround: older layout libraries don't provide a tag for this error
+ if (tag == null && message != null
+ && message.startsWith("Failed to find style ")) { //$NON-NLS-1$
+ tag = LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR;
+ }
+
+ addError(tag, description);
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable throwable, Object data) {
+ String description = describe(message);
+ appendToIdeLog(throwable, IStatus.ERROR, description);
+
+ if (throwable != null) {
+ if (throwable instanceof ClassNotFoundException) {
+ // The project callback is given a chance to resolve classes,
+ // and when it fails, it will record it in its own list which
+ // is displayed in a special way (with action hyperlinks etc).
+ // Therefore, include these messages in the visible render log,
+ // especially since the user message from a ClassNotFoundException
+ // is really not helpful (it just lists the class name without
+ // even mentioning that it is a class-not-found exception.)
+ return;
+ }
+
+ if (description.equals(throwable.getLocalizedMessage()) ||
+ description.equals(throwable.getMessage())) {
+ description = "Exception raised during rendering: " + description;
+ }
+ recordThrowable(throwable);
+ mHaveExceptions = true;
+ }
+
+ addError(tag, description);
+ }
+
+ /**
+ * Record that the given exception was encountered during rendering
+ *
+ * @param throwable the exception that was raised
+ */
+ public void recordThrowable(@NonNull Throwable throwable) {
+ if (mTraces == null) {
+ mTraces = new ArrayList<Throwable>();
+ }
+ mTraces.add(throwable);
+ }
+
+ @Override
+ public void warning(String tag, String message, Object data) {
+ String description = describe(message);
+
+ boolean log = true;
+ if (TAG_RESOURCES_FORMAT.equals(tag)) {
+ if (description.equals("You must supply a layout_width attribute.") //$NON-NLS-1$
+ || description.equals("You must supply a layout_height attribute.")) {//$NON-NLS-1$
+ tag = TAG_MISSING_DIMENSION;
+ log = false;
+ }
+ }
+
+ if (log) {
+ appendToIdeLog(null, IStatus.WARNING, description);
+ }
+
+ addWarning(tag, description);
+ }
+
+ @Override
+ public void fidelityWarning(String tag, String message, Throwable throwable, Object data) {
+ if (sIgnoredFidelityWarnings != null && sIgnoredFidelityWarnings.contains(message)) {
+ return;
+ }
+
+ String description = describe(message);
+ appendToIdeLog(throwable, IStatus.ERROR, description);
+
+ if (throwable != null) {
+ mHaveExceptions = true;
+ }
+
+ addFidelityWarning(tag, description);
+ }
+
+ /**
+ * Ignore the given render fidelity warning for the current session
+ *
+ * @param message the message to be ignored for this session
+ */
+ public static void ignoreFidelityWarning(String message) {
+ if (sIgnoredFidelityWarnings == null) {
+ sIgnoredFidelityWarnings = new HashSet<String>();
+ }
+ sIgnoredFidelityWarnings.add(message);
+ }
+
+ @NonNull
+ private String describe(@Nullable String message) {
+ if (message == null) {
+ return "";
+ } else {
+ return message;
+ }
+ }
+
+ private void addWarning(String tag, String description) {
+ if (mWarnings == null) {
+ mWarnings = new ArrayList<String>();
+ } else if (mWarnings.contains(description)) {
+ // Avoid duplicates
+ return;
+ }
+ mWarnings.add(description);
+ addTag(tag);
+ }
+
+ private void addError(String tag, String description) {
+ if (mErrors == null) {
+ mErrors = new ArrayList<String>();
+ } else if (mErrors.contains(description)) {
+ // Avoid duplicates
+ return;
+ }
+ mErrors.add(description);
+ addTag(tag);
+ }
+
+ private void addFidelityWarning(String tag, String description) {
+ if (mFidelityWarnings == null) {
+ mFidelityWarnings = new ArrayList<String>();
+ } else if (mFidelityWarnings.contains(description)) {
+ // Avoid duplicates
+ return;
+ }
+ mFidelityWarnings.add(description);
+ addTag(tag);
+ }
+
+ // ---- Tags ----
+
+ private void addTag(String tag) {
+ if (tag != null) {
+ if (mTags == null) {
+ mTags = new ArrayList<String>();
+ }
+ mTags.add(tag);
+ }
+ }
+
+ /**
+ * Returns true if the given tag prefix has been seen
+ *
+ * @param prefix the tag prefix to look for
+ * @return true iff any tags with the given prefix was seen during the render
+ */
+ public boolean seenTagPrefix(String prefix) {
+ if (mTags != null) {
+ for (String tag : mTags) {
+ if (tag.startsWith(prefix)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the given tag has been seen
+ *
+ * @param tag the tag to look for
+ * @return true iff the tag was seen during the render
+ */
+ public boolean seenTag(String tag) {
+ if (mTags != null) {
+ return mTags.contains(tag);
+ } else {
+ return false;
+ }
+ }
+
+ // Append the given message to the ADT log. Bypass the sandbox if necessary
+ // such that we can write to the log file.
+ private void appendToIdeLog(Throwable throwable, int severity, String description) {
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ if (throwable != null) {
+ AdtPlugin.log(throwable, "%1$s: %2$s", mName, description);
+ } else {
+ AdtPlugin.log(severity, "%1$s: %2$s", mName, description);
+ }
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
new file mode 100644
index 000000000..5621d5f17
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
@@ -0,0 +1,1333 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_RENDERING;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.Result.Status;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.resources.Density;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.State;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.IJobChangeListener;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Region;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.progress.UIJob;
+import org.w3c.dom.Document;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.lang.ref.SoftReference;
+import java.util.Comparator;
+import java.util.Map;
+
+/**
+ * Represents a preview rendering of a given configuration
+ */
+public class RenderPreview implements IJobChangeListener {
+ /** Whether previews should use large shadows */
+ static final boolean LARGE_SHADOWS = false;
+
+ /**
+ * Still doesn't work; get exceptions from layoutlib:
+ * java.lang.IllegalStateException: After scene creation, #init() must be called
+ * at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151)
+ * <p>
+ * TODO: Investigate.
+ */
+ private static final boolean RENDER_ASYNC = false;
+
+ /**
+ * Height of the toolbar shown over a preview during hover. Needs to be
+ * large enough to accommodate icons below.
+ */
+ private static final int HEADER_HEIGHT = 20;
+
+ /** Whether to dump out rendering failures of the previews to the log */
+ private static final boolean DUMP_RENDER_DIAGNOSTICS = false;
+
+ /** Extra error checking in debug mode */
+ private static final boolean DEBUG = false;
+
+ private static final Image EDIT_ICON;
+ private static final Image ZOOM_IN_ICON;
+ private static final Image ZOOM_OUT_ICON;
+ private static final Image CLOSE_ICON;
+ private static final int EDIT_ICON_WIDTH;
+ private static final int ZOOM_IN_ICON_WIDTH;
+ private static final int ZOOM_OUT_ICON_WIDTH;
+ private static final int CLOSE_ICON_WIDTH;
+ static {
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ IconFactory icons = IconFactory.getInstance();
+ CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE);
+ EDIT_ICON = icons.getIcon("editPreview"); //$NON-NLS-1$
+ ZOOM_IN_ICON = icons.getIcon("zoomplus"); //$NON-NLS-1$
+ ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$
+ CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width;
+ EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width;
+ ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width;
+ ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width;
+ }
+
+ /** The configuration being previewed */
+ private @NonNull Configuration mConfiguration;
+
+ /** Configuration to use if we have an alternate input to be rendered */
+ private @NonNull Configuration mAlternateConfiguration;
+
+ /** The associated manager */
+ private final @NonNull RenderPreviewManager mManager;
+ private final @NonNull LayoutCanvas mCanvas;
+
+ private @NonNull SoftReference<ResourceResolver> mResourceResolver =
+ new SoftReference<ResourceResolver>(null);
+ private @Nullable Job mJob;
+ private @Nullable Image mThumbnail;
+ private @Nullable String mDisplayName;
+ private int mWidth;
+ private int mHeight;
+ private int mX;
+ private int mY;
+ private int mTitleHeight;
+ private double mScale = 1.0;
+ private double mAspectRatio;
+
+ /** If non null, points to a separate file containing the source */
+ private @Nullable IFile mAlternateInput;
+
+ /** If included within another layout, the name of that outer layout */
+ private @Nullable Reference mIncludedWithin;
+
+ /** Whether the mouse is actively hovering over this preview */
+ private boolean mActive;
+
+ /**
+ * Whether this preview cannot be rendered because of a model error - such
+ * as an invalid configuration, a missing resource, an error in the XML
+ * markup, etc. If non null, contains the error message (or a blank string
+ * if not known), and null if the render was successful.
+ */
+ private String mError;
+
+ /** Whether in the current layout, this preview is visible */
+ private boolean mVisible;
+
+ /** Whether the configuration has changed and needs to be refreshed the next time
+ * this preview made visible. This corresponds to the change flags in
+ * {@link ConfigurationClient}. */
+ private int mDirty;
+
+ /**
+ * Creates a new {@linkplain RenderPreview}
+ *
+ * @param manager the manager
+ * @param canvas canvas where preview is painted
+ * @param configuration the associated configuration
+ * @param width the initial width to use for the preview
+ * @param height the initial height to use for the preview
+ */
+ private RenderPreview(
+ @NonNull RenderPreviewManager manager,
+ @NonNull LayoutCanvas canvas,
+ @NonNull Configuration configuration) {
+ mManager = manager;
+ mCanvas = canvas;
+ mConfiguration = configuration;
+ updateSize();
+
+ // Should only attempt to create configurations for fully configured devices
+ assert mConfiguration.getDevice() != null
+ && mConfiguration.getDeviceState() != null
+ && mConfiguration.getLocale() != null
+ && mConfiguration.getTarget() != null
+ && mConfiguration.getTheme() != null
+ && mConfiguration.getFullConfig() != null
+ && mConfiguration.getFullConfig().getScreenSizeQualifier() != null :
+ mConfiguration;
+ }
+
+ /**
+ * Sets the configuration to use for this preview
+ *
+ * @param configuration the new configuration
+ */
+ public void setConfiguration(@NonNull Configuration configuration) {
+ mConfiguration = configuration;
+ }
+
+ /**
+ * Gets the scale being applied to the thumbnail
+ *
+ * @return the scale being applied to the thumbnail
+ */
+ public double getScale() {
+ return mScale;
+ }
+
+ /**
+ * Sets the scale to apply to the thumbnail
+ *
+ * @param scale the factor to scale the thumbnail picture by
+ */
+ public void setScale(double scale) {
+ disposeThumbnail();
+ mScale = scale;
+ }
+
+ /**
+ * Returns the aspect ratio of this render preview
+ *
+ * @return the aspect ratio
+ */
+ public double getAspectRatio() {
+ return mAspectRatio;
+ }
+
+ /**
+ * Returns whether the preview is actively hovered
+ *
+ * @return whether the mouse is hovering over the preview
+ */
+ public boolean isActive() {
+ return mActive;
+ }
+
+ /**
+ * Sets whether the preview is actively hovered
+ *
+ * @param active if the mouse is hovering over the preview
+ */
+ public void setActive(boolean active) {
+ mActive = active;
+ }
+
+ /**
+ * Returns whether the preview is visible. Previews that are off
+ * screen are typically marked invisible during layout, which means we don't
+ * have to expend effort computing preview thumbnails etc
+ *
+ * @return true if the preview is visible
+ */
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ /**
+ * Returns whether this preview represents a forked layout
+ *
+ * @return true if this preview represents a separate file
+ */
+ public boolean isForked() {
+ return mAlternateInput != null || mIncludedWithin != null;
+ }
+
+ /**
+ * Returns the file to be used for this preview, or null if this is not a
+ * forked layout meaning that the file is the one used in the chooser
+ *
+ * @return the file or null for non-forked layouts
+ */
+ @Nullable
+ public IFile getAlternateInput() {
+ if (mAlternateInput != null) {
+ return mAlternateInput;
+ } else if (mIncludedWithin != null) {
+ return mIncludedWithin.getFile();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the area of this render preview, PRIOR to scaling
+ *
+ * @return the area (width times height without scaling)
+ */
+ int getArea() {
+ return mWidth * mHeight;
+ }
+
+ /**
+ * Sets whether the preview is visible. Previews that are off
+ * screen are typically marked invisible during layout, which means we don't
+ * have to expend effort computing preview thumbnails etc
+ *
+ * @param visible whether this preview is visible
+ */
+ public void setVisible(boolean visible) {
+ if (visible != mVisible) {
+ mVisible = visible;
+ if (mVisible) {
+ if (mDirty != 0) {
+ // Just made the render preview visible:
+ configurationChanged(mDirty); // schedules render
+ } else {
+ updateForkStatus();
+ mManager.scheduleRender(this);
+ }
+ } else {
+ dispose();
+ }
+ }
+ }
+
+ /**
+ * Sets the layout position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ void setPosition(int x, int y) {
+ mX = x;
+ mY = y;
+ }
+
+ /**
+ * Gets the layout X position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ int getX() {
+ return mX;
+ }
+
+ /**
+ * Gets the layout Y position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ int getY() {
+ return mY;
+ }
+
+ /** Determine whether this configuration has a better match in a different layout file */
+ private void updateForkStatus() {
+ ConfigurationChooser chooser = mManager.getChooser();
+ FolderConfiguration config = mConfiguration.getFullConfig();
+ if (mAlternateInput != null && chooser.isBestMatchFor(mAlternateInput, config)) {
+ return;
+ }
+
+ mAlternateInput = null;
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile != null) {
+ if (!chooser.isBestMatchFor(editedFile, config)) {
+ ProjectResources resources = chooser.getResources();
+ if (resources != null) {
+ ResourceFile best = resources.getMatchingFile(editedFile.getName(),
+ ResourceType.LAYOUT, config);
+ if (best != null) {
+ IAbstractFile file = best.getFile();
+ if (file instanceof IFileWrapper) {
+ mAlternateInput = ((IFileWrapper) file).getIFile();
+ } else if (file instanceof File) {
+ mAlternateInput = AdtUtils.fileToIFile(((File) file));
+ }
+ }
+ }
+ if (mAlternateInput != null) {
+ mAlternateConfiguration = Configuration.create(mConfiguration,
+ mAlternateInput);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a new {@linkplain RenderPreview}
+ *
+ * @param manager the manager
+ * @param configuration the associated configuration
+ * @return a new configuration
+ */
+ @NonNull
+ public static RenderPreview create(
+ @NonNull RenderPreviewManager manager,
+ @NonNull Configuration configuration) {
+ LayoutCanvas canvas = manager.getCanvas();
+ return new RenderPreview(manager, canvas, configuration);
+ }
+
+ /**
+ * Throws away this preview: cancels any pending rendering jobs and disposes
+ * of image resources etc
+ */
+ public void dispose() {
+ disposeThumbnail();
+
+ if (mJob != null) {
+ mJob.cancel();
+ mJob = null;
+ }
+ }
+
+ /** Disposes the thumbnail rendering. */
+ void disposeThumbnail() {
+ if (mThumbnail != null) {
+ mThumbnail.dispose();
+ mThumbnail = null;
+ }
+ }
+
+ /**
+ * Returns the display name of this preview
+ *
+ * @return the name of the preview
+ */
+ @NonNull
+ public String getDisplayName() {
+ if (mDisplayName == null) {
+ String displayName = getConfiguration().getDisplayName();
+ if (displayName == null) {
+ // No display name: this must be the configuration used by default
+ // for the view which is originally displayed (before adding thumbnails),
+ // and you've switched away to something else; now we need to display a name
+ // for this original configuration. For now, just call it "Original"
+ return "Original";
+ }
+
+ return displayName;
+ }
+
+ return mDisplayName;
+ }
+
+ /**
+ * Sets the display name of this preview. By default, the display name is
+ * the display name of the configuration, but it can be overridden by calling
+ * this setter (which only sets the preview name, without editing the configuration.)
+ *
+ * @param displayName the new display name
+ */
+ public void setDisplayName(@NonNull String displayName) {
+ mDisplayName = displayName;
+ }
+
+ /**
+ * Sets an inclusion context to use for this layout, if any. This will render
+ * the configuration preview as the outer layout with the current layout
+ * embedded within.
+ *
+ * @param includedWithin a reference to a layout which includes this one
+ */
+ public void setIncludedWithin(Reference includedWithin) {
+ mIncludedWithin = includedWithin;
+ }
+
+ /**
+ * Request a new render after the given delay
+ *
+ * @param delay the delay to wait before starting the render job
+ */
+ public void render(long delay) {
+ Job job = mJob;
+ if (job != null) {
+ job.cancel();
+ }
+ if (RENDER_ASYNC) {
+ job = new AsyncRenderJob();
+ } else {
+ job = new RenderJob();
+ }
+ job.schedule(delay);
+ job.addJobChangeListener(this);
+ mJob = job;
+ }
+
+ /** Render immediately */
+ private void renderSync() {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (editor.getReadyLayoutLib(false /*displayError*/) == null) {
+ // Don't attempt to render when there is no ready layout library: most likely
+ // the targets are loading/reloading.
+ return;
+ }
+
+ disposeThumbnail();
+
+ Configuration configuration =
+ mAlternateInput != null && mAlternateConfiguration != null
+ ? mAlternateConfiguration : mConfiguration;
+ ResourceResolver resolver = getResourceResolver(configuration);
+ RenderService renderService = RenderService.create(editor, configuration, resolver);
+
+ if (mIncludedWithin != null) {
+ renderService.setIncludedWithin(mIncludedWithin);
+ }
+
+ if (mAlternateInput != null) {
+ IAndroidTarget target = editor.getRenderingTarget();
+ AndroidTargetData data = null;
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ data = sdk.getTargetData(target);
+ }
+ }
+
+ // Construct UI model from XML
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(mCanvas.getEditorDelegate().getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+
+ Document document = DomUtilities.getDocument(mAlternateInput);
+ if (document == null) {
+ mError = "No document";
+ createErrorThumbnail();
+ return;
+ }
+ model.loadFromXmlNode(document);
+ renderService.setModel(model);
+ } else {
+ renderService.setModel(editor.getModel());
+ }
+ RenderLogger log = editor.createRenderLogger(getDisplayName());
+ renderService.setLog(log);
+ RenderSession session = renderService.createRenderSession();
+ Result render = session.render(1000);
+
+ if (DUMP_RENDER_DIAGNOSTICS) {
+ if (log.hasProblems() || !render.isSuccess()) {
+ AdtPlugin.log(IStatus.ERROR, "Found problems rendering preview "
+ + getDisplayName() + ": "
+ + render.getErrorMessage() + " : "
+ + log.getProblems(false));
+ Throwable exception = render.getException();
+ if (exception != null) {
+ AdtPlugin.log(exception, "Failure rendering preview " + getDisplayName());
+ }
+ }
+ }
+
+ if (render.isSuccess()) {
+ mError = null;
+ } else {
+ mError = render.getErrorMessage();
+ if (mError == null) {
+ mError = "";
+ }
+ }
+
+ if (render.getStatus() == Status.ERROR_TIMEOUT) {
+ // TODO: Special handling? schedule update again later
+ return;
+ }
+ if (render.isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null) {
+ createThumbnail(image);
+ }
+ }
+
+ if (mError != null) {
+ createErrorThumbnail();
+ }
+ }
+
+ private ResourceResolver getResourceResolver(Configuration configuration) {
+ ResourceResolver resourceResolver = mResourceResolver.get();
+ if (resourceResolver != null) {
+ return resourceResolver;
+ }
+
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ String theme = configuration.getTheme();
+ if (theme == null) {
+ return null;
+ }
+
+ Map<ResourceType, Map<String, ResourceValue>> configuredFrameworkRes = null;
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = null;
+
+ FolderConfiguration config = configuration.getFullConfig();
+ IAndroidTarget target = graphicalEditor.getRenderingTarget();
+ ResourceRepository frameworkRes = null;
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk == null) {
+ return null;
+ }
+ AndroidTargetData data = sdk.getTargetData(target);
+
+ if (data != null) {
+ // TODO: SHARE if possible
+ frameworkRes = data.getFrameworkResources();
+ configuredFrameworkRes = frameworkRes.getConfiguredResources(config);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ assert configuredFrameworkRes != null;
+
+
+ // get the resources of the file's project.
+ ProjectResources projectRes = ResourceManager.getInstance().getProjectResources(
+ graphicalEditor.getProject());
+ configuredProjectRes = projectRes.getConfiguredResources(config);
+
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ } else {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+
+ resourceResolver = ResourceResolver.create(
+ configuredProjectRes, configuredFrameworkRes,
+ ResourceHelper.styleToTheme(theme),
+ ResourceHelper.isProjectStyle(theme));
+ mResourceResolver = new SoftReference<ResourceResolver>(resourceResolver);
+ return resourceResolver;
+ }
+
+ /**
+ * Sets the new image of the preview and generates a thumbnail
+ *
+ * @param image the full size image
+ */
+ void createThumbnail(BufferedImage image) {
+ if (image == null) {
+ mThumbnail = null;
+ return;
+ }
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
+ double scale = getWidth() / (double) image.getWidth();
+ int shadowSize;
+ if (LARGE_SHADOWS) {
+ shadowSize = drawShadows ? SHADOW_SIZE : 0;
+ } else {
+ shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0;
+ }
+ if (scale < 1.0) {
+ if (LARGE_SHADOWS) {
+ image = ImageUtils.scale(image, scale, scale,
+ shadowSize, shadowSize);
+ if (drawShadows) {
+ ImageUtils.drawRectangleShadow(image, 0, 0,
+ image.getWidth() - shadowSize,
+ image.getHeight() - shadowSize);
+ }
+ } else {
+ image = ImageUtils.scale(image, scale, scale,
+ shadowSize, shadowSize);
+ if (drawShadows) {
+ ImageUtils.drawSmallRectangleShadow(image, 0, 0,
+ image.getWidth() - shadowSize,
+ image.getHeight() - shadowSize);
+ }
+ }
+ }
+
+ mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
+ true /* transferAlpha */, -1);
+ }
+
+ void createErrorThumbnail() {
+ int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
+ int width = getWidth();
+ int height = getHeight();
+ BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g = image.createGraphics();
+ g.setColor(new java.awt.Color(0xfffbfcc6));
+ g.fillRect(0, 0, width, height);
+
+ g.dispose();
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
+ if (drawShadows) {
+ if (LARGE_SHADOWS) {
+ ImageUtils.drawRectangleShadow(image, 0, 0,
+ image.getWidth() - SHADOW_SIZE,
+ image.getHeight() - SHADOW_SIZE);
+ } else {
+ ImageUtils.drawSmallRectangleShadow(image, 0, 0,
+ image.getWidth() - SMALL_SHADOW_SIZE,
+ image.getHeight() - SMALL_SHADOW_SIZE);
+ }
+ }
+
+ mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
+ true /* transferAlpha */, -1);
+ }
+
+ private static double getScale(int width, int height) {
+ int maxWidth = RenderPreviewManager.getMaxWidth();
+ int maxHeight = RenderPreviewManager.getMaxHeight();
+ if (width > 0 && height > 0
+ && (width > maxWidth || height > maxHeight)) {
+ if (width >= height) { // landscape
+ return maxWidth / (double) width;
+ } else { // portrait
+ return maxHeight / (double) height;
+ }
+ }
+
+ return 1.0;
+ }
+
+ /**
+ * Returns the width of the preview, in pixels
+ *
+ * @return the width in pixels
+ */
+ public int getWidth() {
+ return (int) (mWidth * mScale * RenderPreviewManager.getScale());
+ }
+
+ /**
+ * Returns the height of the preview, in pixels
+ *
+ * @return the height in pixels
+ */
+ public int getHeight() {
+ return (int) (mHeight * mScale * RenderPreviewManager.getScale());
+ }
+
+ /**
+ * Handles clicks within the preview (x and y are positions relative within the
+ * preview
+ *
+ * @param x the x coordinate within the preview where the click occurred
+ * @param y the y coordinate within the preview where the click occurred
+ * @return true if this preview handled (and therefore consumed) the click
+ */
+ public boolean click(int x, int y) {
+ if (y >= mTitleHeight && y < mTitleHeight + HEADER_HEIGHT) {
+ int left = 0;
+ left += CLOSE_ICON_WIDTH;
+ if (x <= left) {
+ // Delete
+ mManager.deletePreview(this);
+ return true;
+ }
+ left += ZOOM_IN_ICON_WIDTH;
+ if (x <= left) {
+ // Zoom in
+ mScale = mScale * (1 / 0.5);
+ if (Math.abs(mScale-1.0) < 0.0001) {
+ mScale = 1.0;
+ }
+
+ render(0);
+ mManager.layout(true);
+ mCanvas.redraw();
+ return true;
+ }
+ left += ZOOM_OUT_ICON_WIDTH;
+ if (x <= left) {
+ // Zoom out
+ mScale = mScale * (0.5 / 1);
+ if (Math.abs(mScale-1.0) < 0.0001) {
+ mScale = 1.0;
+ }
+ render(0);
+
+ mManager.layout(true);
+ mCanvas.redraw();
+ return true;
+ }
+ left += EDIT_ICON_WIDTH;
+ if (x <= left) {
+ // Edit. For now, just rename
+ InputDialog d = new InputDialog(
+ AdtPlugin.getShell(),
+ "Rename Preview", // title
+ "Name:",
+ getDisplayName(),
+ null);
+ if (d.open() == Window.OK) {
+ String newName = d.getValue();
+ mConfiguration.setDisplayName(newName);
+ if (mDescription != null) {
+ mManager.rename(mDescription, newName);
+ }
+ mCanvas.redraw();
+ }
+
+ return true;
+ }
+
+ // Clicked anywhere else on header
+ // Perhaps open Edit dialog here?
+ }
+
+ mManager.switchTo(this);
+ return true;
+ }
+
+ /**
+ * Paints the preview at the given x/y position
+ *
+ * @param gc the graphics context to paint it into
+ * @param x the x coordinate to paint the preview at
+ * @param y the y coordinate to paint the preview at
+ */
+ void paint(GC gc, int x, int y) {
+ mTitleHeight = paintTitle(gc, x, y, true /*showFile*/);
+ y += mTitleHeight;
+ y += 2;
+
+ int width = getWidth();
+ int height = getHeight();
+ if (mThumbnail != null && mError == null) {
+ gc.drawImage(mThumbnail, x, y);
+
+ if (mActive) {
+ int oldWidth = gc.getLineWidth();
+ gc.setLineWidth(3);
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION));
+ gc.drawRectangle(x - 1, y - 1, width + 2, height + 2);
+ gc.setLineWidth(oldWidth);
+ }
+ } else if (mError != null) {
+ if (mThumbnail != null) {
+ gc.drawImage(mThumbnail, x, y);
+ } else {
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+ gc.drawRectangle(x, y, width, height);
+ }
+
+ gc.setClipping(x, y, width, height);
+ Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$
+ ImageData data = icon.getImageData();
+ int prevAlpha = gc.getAlpha();
+ int alpha = 96;
+ if (mThumbnail != null) {
+ alpha -= 32;
+ }
+ gc.setAlpha(alpha);
+ gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2);
+
+ String msg = mError;
+ Density density = mConfiguration.getDensity();
+ if (density == Density.TV || density == Density.LOW) {
+ msg = "Broken rendering library; unsupported DPI. Try using the SDK manager " +
+ "to get updated layout libraries.";
+ }
+ int charWidth = gc.getFontMetrics().getAverageCharWidth();
+ int charsPerLine = (width - 10) / charWidth;
+ msg = SdkUtils.wrap(msg, charsPerLine, null);
+ gc.setAlpha(255);
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK));
+ gc.drawText(msg, x + 5, y + HEADER_HEIGHT, true);
+ gc.setAlpha(prevAlpha);
+ gc.setClipping((Region) null);
+ } else {
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+ gc.drawRectangle(x, y, width, height);
+
+ Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$
+ ImageData data = icon.getImageData();
+ int prevAlpha = gc.getAlpha();
+ gc.setAlpha(96);
+ gc.drawImage(icon, x + (width - data.width) / 2,
+ y + (height - data.height) / 2);
+ gc.setAlpha(prevAlpha);
+ }
+
+ if (mActive) {
+ int left = x ;
+ int prevAlpha = gc.getAlpha();
+ gc.setAlpha(208);
+ Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE);
+ gc.setBackground(bg);
+ gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT);
+ gc.setAlpha(prevAlpha);
+
+ y += 2;
+
+ // Paint icons
+ gc.drawImage(CLOSE_ICON, left, y);
+ left += CLOSE_ICON_WIDTH;
+
+ gc.drawImage(ZOOM_IN_ICON, left, y);
+ left += ZOOM_IN_ICON_WIDTH;
+
+ gc.drawImage(ZOOM_OUT_ICON, left, y);
+ left += ZOOM_OUT_ICON_WIDTH;
+
+ gc.drawImage(EDIT_ICON, left, y);
+ left += EDIT_ICON_WIDTH;
+ }
+ }
+
+ /**
+ * Paints the preview title at the given position (and returns the required
+ * height)
+ *
+ * @param gc the graphics context to paint into
+ * @param x the left edge of the preview rectangle
+ * @param y the top edge of the preview rectangle
+ */
+ private int paintTitle(GC gc, int x, int y, boolean showFile) {
+ String displayName = getDisplayName();
+ return paintTitle(gc, x, y, showFile, displayName);
+ }
+
+ /**
+ * Paints the preview title at the given position (and returns the required
+ * height)
+ *
+ * @param gc the graphics context to paint into
+ * @param x the left edge of the preview rectangle
+ * @param y the top edge of the preview rectangle
+ * @param displayName the title string to be used
+ */
+ int paintTitle(GC gc, int x, int y, boolean showFile, String displayName) {
+ int titleHeight = 0;
+
+ if (showFile && mIncludedWithin != null) {
+ if (mManager.getMode() != INCLUDES) {
+ displayName = "<include>";
+ } else {
+ // Skip: just paint footer instead
+ displayName = null;
+ }
+ }
+
+ int width = getWidth();
+ int labelTop = y + 1;
+ gc.setClipping(x, labelTop, width, 100);
+
+ // Use font height rather than extent height since we want two adjacent
+ // previews (which may have different display names and therefore end
+ // up with slightly different extent heights) to have identical title
+ // heights such that they are aligned identically
+ int fontHeight = gc.getFontMetrics().getHeight();
+
+ if (displayName != null && displayName.length() > 0) {
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE));
+ Point extent = gc.textExtent(displayName);
+ int labelLeft = Math.max(x, x + (width - extent.x) / 2);
+ Image icon = null;
+ Locale locale = mConfiguration.getLocale();
+ if (locale != null && (locale.hasLanguage() || locale.hasRegion())
+ && (!(mConfiguration instanceof NestedConfiguration)
+ || ((NestedConfiguration) mConfiguration).isOverridingLocale())) {
+ icon = locale.getFlagImage();
+ }
+
+ if (icon != null) {
+ int flagWidth = icon.getImageData().width;
+ int flagHeight = icon.getImageData().height;
+ labelLeft = Math.max(x + flagWidth / 2, labelLeft);
+ gc.drawImage(icon, labelLeft - flagWidth / 2 - 1, labelTop);
+ labelLeft += flagWidth / 2 + 1;
+ gc.drawText(displayName, labelLeft,
+ labelTop - (extent.y - flagHeight) / 2, true);
+ } else {
+ gc.drawText(displayName, labelLeft, labelTop, true);
+ }
+
+ labelTop += extent.y;
+ titleHeight += fontHeight;
+ }
+
+ if (showFile && (mAlternateInput != null || mIncludedWithin != null)) {
+ // Draw file flag, and parent folder name
+ IFile file = mAlternateInput != null
+ ? mAlternateInput : mIncludedWithin.getFile();
+ String fileName = file.getParent().getName() + File.separator
+ + file.getName();
+ Point extent = gc.textExtent(fileName);
+ Image icon = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$
+ int flagWidth = icon.getImageData().width;
+ int flagHeight = icon.getImageData().height;
+
+ int labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2);
+
+ gc.drawImage(icon, labelLeft, labelTop);
+
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ labelLeft += flagWidth + 1;
+ labelTop -= (extent.y - flagHeight) / 2;
+ gc.drawText(fileName, labelLeft, labelTop, true);
+
+ titleHeight += Math.max(titleHeight, icon.getImageData().height);
+ }
+
+ gc.setClipping((Region) null);
+
+ return titleHeight;
+ }
+
+ /**
+ * Notifies that the preview's configuration has changed.
+ *
+ * @param flags the change flags, a bitmask corresponding to the
+ * {@code CHANGE_} constants in {@link ConfigurationClient}
+ */
+ public void configurationChanged(int flags) {
+ if (!mVisible) {
+ mDirty |= flags;
+ return;
+ }
+
+ if ((flags & MASK_RENDERING) != 0) {
+ mResourceResolver.clear();
+ // Handle inheritance
+ mConfiguration.syncFolderConfig();
+ updateForkStatus();
+ updateSize();
+ }
+
+ // Sanity check to make sure things are working correctly
+ if (DEBUG) {
+ RenderPreviewMode mode = mManager.getMode();
+ if (mode == DEFAULT) {
+ assert mConfiguration instanceof VaryingConfiguration;
+ VaryingConfiguration config = (VaryingConfiguration) mConfiguration;
+ int alternateFlags = config.getAlternateFlags();
+ switch (alternateFlags) {
+ case Configuration.CFG_DEVICE_STATE: {
+ State configState = config.getDeviceState();
+ State chooserState = mManager.getChooser().getConfiguration()
+ .getDeviceState();
+ assert configState != null && chooserState != null;
+ assert !configState.getName().equals(chooserState.getName())
+ : configState.toString() + ':' + chooserState;
+
+ Device configDevice = config.getDevice();
+ Device chooserDevice = mManager.getChooser().getConfiguration()
+ .getDevice();
+ assert configDevice != null && chooserDevice != null;
+ assert configDevice == chooserDevice
+ : configDevice.toString() + ':' + chooserDevice;
+
+ break;
+ }
+ case Configuration.CFG_DEVICE: {
+ Device configDevice = config.getDevice();
+ Device chooserDevice = mManager.getChooser().getConfiguration()
+ .getDevice();
+ assert configDevice != null && chooserDevice != null;
+ assert configDevice != chooserDevice
+ : configDevice.toString() + ':' + chooserDevice;
+
+ State configState = config.getDeviceState();
+ State chooserState = mManager.getChooser().getConfiguration()
+ .getDeviceState();
+ assert configState != null && chooserState != null;
+ assert configState.getName().equals(chooserState.getName())
+ : configState.toString() + ':' + chooserState;
+
+ break;
+ }
+ case Configuration.CFG_LOCALE: {
+ Locale configLocale = config.getLocale();
+ Locale chooserLocale = mManager.getChooser().getConfiguration()
+ .getLocale();
+ assert configLocale != null && chooserLocale != null;
+ assert configLocale != chooserLocale
+ : configLocale.toString() + ':' + chooserLocale;
+ break;
+ }
+ default: {
+ // Some other type of override I didn't anticipate
+ assert false : alternateFlags;
+ }
+ }
+ }
+ }
+
+ mDirty = 0;
+ mManager.scheduleRender(this);
+ }
+
+ private void updateSize() {
+ Device device = mConfiguration.getDevice();
+ if (device == null) {
+ return;
+ }
+ Screen screen = device.getDefaultHardware().getScreen();
+ if (screen == null) {
+ return;
+ }
+
+ FolderConfiguration folderConfig = mConfiguration.getFullConfig();
+ ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier();
+ ScreenOrientation orientation = qualifier == null
+ ? ScreenOrientation.PORTRAIT : qualifier.getValue();
+
+ // compute width and height to take orientation into account.
+ int x = screen.getXDimension();
+ int y = screen.getYDimension();
+ int screenWidth, screenHeight;
+
+ if (x > y) {
+ if (orientation == ScreenOrientation.LANDSCAPE) {
+ screenWidth = x;
+ screenHeight = y;
+ } else {
+ screenWidth = y;
+ screenHeight = x;
+ }
+ } else {
+ if (orientation == ScreenOrientation.LANDSCAPE) {
+ screenWidth = y;
+ screenHeight = x;
+ } else {
+ screenWidth = x;
+ screenHeight = y;
+ }
+ }
+
+ int width = RenderPreviewManager.getMaxWidth();
+ int height = RenderPreviewManager.getMaxHeight();
+ if (screenWidth > 0) {
+ double scale = getScale(screenWidth, screenHeight);
+ width = (int) (screenWidth * scale);
+ height = (int) (screenHeight * scale);
+ }
+
+ if (width != mWidth || height != mHeight) {
+ mWidth = width;
+ mHeight = height;
+
+ Image thumbnail = mThumbnail;
+ mThumbnail = null;
+ if (thumbnail != null) {
+ thumbnail.dispose();
+ }
+ if (mHeight != 0) {
+ mAspectRatio = mWidth / (double) mHeight;
+ }
+ }
+ }
+
+ /**
+ * Returns the configuration associated with this preview
+ *
+ * @return the configuration
+ */
+ @NonNull
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ // ---- Implements IJobChangeListener ----
+
+ @Override
+ public void aboutToRun(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void awake(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void done(IJobChangeEvent event) {
+ mJob = null;
+ }
+
+ @Override
+ public void running(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void scheduled(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void sleeping(IJobChangeEvent event) {
+ }
+
+ // ---- Delayed Rendering ----
+
+ private final class RenderJob extends UIJob {
+ public RenderJob() {
+ super("RenderPreview");
+ setSystem(true);
+ setUser(false);
+ }
+
+ @Override
+ public IStatus runInUIThread(IProgressMonitor monitor) {
+ mJob = null;
+ if (!mCanvas.isDisposed()) {
+ renderSync();
+ mCanvas.redraw();
+ return org.eclipse.core.runtime.Status.OK_STATUS;
+ }
+
+ return org.eclipse.core.runtime.Status.CANCEL_STATUS;
+ }
+
+ @Override
+ public Display getDisplay() {
+ if (mCanvas.isDisposed()) {
+ return null;
+ }
+ return mCanvas.getDisplay();
+ }
+ }
+
+ private final class AsyncRenderJob extends Job {
+ public AsyncRenderJob() {
+ super("RenderPreview");
+ setSystem(true);
+ setUser(false);
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ mJob = null;
+
+ if (mCanvas.isDisposed()) {
+ return org.eclipse.core.runtime.Status.CANCEL_STATUS;
+ }
+
+ renderSync();
+
+ // Update display
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mCanvas.redraw();
+ }
+ });
+
+ return org.eclipse.core.runtime.Status.OK_STATUS;
+ }
+ }
+
+ /**
+ * Sets the input file to use for rendering. If not set, this will just be
+ * the same file as the configuration chooser. This is used to render other
+ * layouts, such as variations of the currently edited layout, which are
+ * not kept in sync with the main layout.
+ *
+ * @param file the file to set as input
+ */
+ public void setAlternateInput(@Nullable IFile file) {
+ mAlternateInput = file;
+ }
+
+ /** Corresponding description for this preview if it is a manually added preview */
+ private @Nullable ConfigurationDescription mDescription;
+
+ /**
+ * Sets the description of this preview, if this preview is a manually added preview
+ *
+ * @param description the description of this preview
+ */
+ public void setDescription(@Nullable ConfigurationDescription description) {
+ mDescription = description;
+ }
+
+ /**
+ * Returns the description of this preview, if this preview is a manually added preview
+ *
+ * @return the description
+ */
+ @Nullable
+ public ConfigurationDescription getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public String toString() {
+ return getDisplayName() + ':' + mConfiguration;
+ }
+
+ /** Sorts render previews into increasing aspect ratio order */
+ static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio);
+ }
+ };
+ /** Sorts render previews into visual order: row by row, column by column */
+ static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ int delta = preview1.mY - preview2.mY;
+ if (delta == 0) {
+ delta = preview1.mX - preview2.mX;
+ }
+ return delta;
+ }
+ };
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java
new file mode 100644
index 000000000..2bcdba382
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.sdklib.devices.Device;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.QualifiedName;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** A list of render previews */
+class RenderPreviewList {
+ /** Name of file saved in project directory storing previews */
+ private static final String PREVIEW_FILE_NAME = "previews.xml"; //$NON-NLS-1$
+
+ /** Qualified name for the per-project persistent property include-map */
+ private final static QualifiedName PREVIEW_LIST = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "previewlist");//$NON-NLS-1$
+
+ private final IProject mProject;
+ private final List<ConfigurationDescription> mList = Lists.newArrayList();
+
+ private RenderPreviewList(@NonNull IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link RenderPreviewList} for the given project
+ *
+ * @param project the project the list is associated with
+ * @return a {@link RenderPreviewList} for the given project, never null
+ */
+ @NonNull
+ public static RenderPreviewList get(@NonNull IProject project) {
+ RenderPreviewList list = null;
+ try {
+ list = (RenderPreviewList) project.getSessionProperty(PREVIEW_LIST);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (list == null) {
+ list = new RenderPreviewList(project);
+ try {
+ project.setSessionProperty(PREVIEW_LIST, list);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return list;
+ }
+
+ private File getManualFile() {
+ return new File(AdtUtils.getAbsolutePath(mProject).toFile(), PREVIEW_FILE_NAME);
+ }
+
+ void load(Collection<Device> deviceList) throws IOException {
+ File file = getManualFile();
+ if (file.exists()) {
+ load(file, deviceList);
+ }
+ }
+
+ void save() throws IOException {
+ deleteFile();
+ if (!mList.isEmpty()) {
+ File file = getManualFile();
+ save(file);
+ }
+ }
+
+ private void save(File file) throws IOException {
+ //Document document = DomUtilities.createEmptyPlainDocument();
+ Document document = DomUtilities.createEmptyDocument();
+ if (document != null) {
+ for (ConfigurationDescription description : mList) {
+ description.toXml(document);
+ }
+ String xml = EclipseXmlPrettyPrinter.prettyPrint(document, true);
+ Files.write(xml, file, Charsets.UTF_8);
+ }
+ }
+
+ void load(File file, Collection<Device> deviceList) throws IOException {
+ mList.clear();
+
+ String xml = Files.toString(file, Charsets.UTF_8);
+ Document document = DomUtilities.parseDocument(xml, true);
+ if (document == null || document.getDocumentElement() == null) {
+ return;
+ }
+ List<Element> elements = DomUtilities.getChildren(document.getDocumentElement());
+ for (Element element : elements) {
+ ConfigurationDescription description = ConfigurationDescription.fromXml(
+ mProject, element, deviceList);
+ if (description != null) {
+ mList.add(description);
+ }
+ }
+ }
+
+ /**
+ * Create a list of previews for the given canvas that matches the internal
+ * configuration preview list
+ *
+ * @param canvas the associated canvas
+ * @return a new list of previews linked to the given canvas
+ */
+ @NonNull
+ List<RenderPreview> createPreviews(LayoutCanvas canvas) {
+ if (mList.isEmpty()) {
+ return new ArrayList<RenderPreview>();
+ }
+ List<RenderPreview> previews = Lists.newArrayList();
+ RenderPreviewManager manager = canvas.getPreviewManager();
+ ConfigurationChooser chooser = canvas.getEditorDelegate().getGraphicalEditor()
+ .getConfigurationChooser();
+
+ Configuration chooserConfig = chooser.getConfiguration();
+ for (ConfigurationDescription description : mList) {
+ Configuration configuration = Configuration.create(chooser);
+ configuration.setDisplayName(description.displayName);
+ configuration.setActivity(description.activity);
+ configuration.setLocale(
+ description.locale != null ? description.locale : chooserConfig.getLocale(),
+ true);
+ // TODO: Make sure this layout isn't in some v-folder which is incompatible
+ // with this target!
+ configuration.setTarget(
+ description.target != null ? description.target : chooserConfig.getTarget(),
+ true);
+ configuration.setTheme(
+ description.theme != null ? description.theme : chooserConfig.getTheme());
+ configuration.setDevice(
+ description.device != null ? description.device : chooserConfig.getDevice(),
+ true);
+ configuration.setDeviceState(
+ description.state != null ? description.state : chooserConfig.getDeviceState(),
+ true);
+ configuration.setNightMode(
+ description.nightMode != null ? description.nightMode
+ : chooserConfig.getNightMode(), true);
+ configuration.setUiMode(
+ description.uiMode != null ? description.uiMode : chooserConfig.getUiMode(), true);
+
+ //configuration.syncFolderConfig();
+ configuration.getFullConfig().set(description.folder);
+
+ RenderPreview preview = RenderPreview.create(manager, configuration);
+
+ preview.setDescription(description);
+ previews.add(preview);
+ }
+
+ return previews;
+ }
+
+ void remove(@NonNull RenderPreview preview) {
+ ConfigurationDescription description = preview.getDescription();
+ if (description != null) {
+ mList.remove(description);
+ }
+ }
+
+ boolean isEmpty() {
+ return mList.isEmpty();
+ }
+
+ void add(@NonNull RenderPreview preview) {
+ Configuration configuration = preview.getConfiguration();
+ ConfigurationDescription description =
+ ConfigurationDescription.fromConfiguration(mProject, configuration);
+ // RenderPreviews can have display names that aren't reflected in the configuration
+ description.displayName = preview.getDisplayName();
+ mList.add(description);
+ preview.setDescription(description);
+ }
+
+ void delete() {
+ mList.clear();
+ deleteFile();
+ }
+
+ private void deleteFile() {
+ File file = getManualFile();
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java
new file mode 100644
index 000000000..98dde86e0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java
@@ -0,0 +1,1696 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreview.LARGE_SHADOWS;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.resources.Density;
+import com.android.resources.ScreenSize;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.State;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Manager for the configuration previews, which handles layout computations,
+ * managing the image buffer cache, etc
+ */
+public class RenderPreviewManager {
+ private static double sScale = 1.0;
+ private static final int RENDER_DELAY = 150;
+ private static final int PREVIEW_VGAP = 18;
+ private static final int PREVIEW_HGAP = 12;
+ private static final int MAX_WIDTH = 200;
+ private static final int MAX_HEIGHT = MAX_WIDTH;
+ private static final int ZOOM_ICON_WIDTH = 16;
+ private static final int ZOOM_ICON_HEIGHT = 16;
+ private @Nullable List<RenderPreview> mPreviews;
+ private @Nullable RenderPreviewList mManualList;
+ private final @NonNull LayoutCanvas mCanvas;
+ private final @NonNull CanvasTransform mVScale;
+ private final @NonNull CanvasTransform mHScale;
+ private int mPrevCanvasWidth;
+ private int mPrevCanvasHeight;
+ private int mPrevImageWidth;
+ private int mPrevImageHeight;
+ private @NonNull RenderPreviewMode mMode = NONE;
+ private @Nullable RenderPreview mActivePreview;
+ private @Nullable ScrollBarListener mListener;
+ private int mLayoutHeight;
+ /** Last seen state revision in this {@link RenderPreviewManager}. If less
+ * than {@link #sRevision}, the previews need to be updated on next exposure */
+ private static int mRevision;
+ /** Current global revision count */
+ private static int sRevision;
+ private boolean mNeedLayout;
+ private boolean mNeedRender;
+ private boolean mNeedZoom;
+ private SwapAnimation mAnimation;
+
+ /**
+ * Creates a {@link RenderPreviewManager} associated with the given canvas
+ *
+ * @param canvas the canvas to manage previews for
+ */
+ public RenderPreviewManager(@NonNull LayoutCanvas canvas) {
+ mCanvas = canvas;
+ mHScale = canvas.getHorizontalTransform();
+ mVScale = canvas.getVerticalTransform();
+ }
+
+ /**
+ * Revise the global state revision counter. This will cause all layout
+ * preview managers to refresh themselves to the latest revision when they
+ * are next exposed.
+ */
+ public static void bumpRevision() {
+ sRevision++;
+ }
+
+ /**
+ * Returns the associated chooser
+ *
+ * @return the associated chooser
+ */
+ @NonNull
+ ConfigurationChooser getChooser() {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ return editor.getConfigurationChooser();
+ }
+
+ /**
+ * Returns the associated canvas
+ *
+ * @return the canvas
+ */
+ @NonNull
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /** Zooms in (grows all previews) */
+ public void zoomIn() {
+ sScale = sScale * (1 / 0.9);
+ if (Math.abs(sScale-1.0) < 0.0001) {
+ sScale = 1.0;
+ }
+
+ updatedZoom();
+ }
+
+ /** Zooms out (shrinks all previews) */
+ public void zoomOut() {
+ sScale = sScale * (0.9 / 1);
+ if (Math.abs(sScale-1.0) < 0.0001) {
+ sScale = 1.0;
+ }
+ updatedZoom();
+ }
+
+ /** Zooms to 100 (resets zoom) */
+ public void zoomReset() {
+ sScale = 1.0;
+ updatedZoom();
+ mNeedZoom = mNeedLayout = true;
+ mCanvas.redraw();
+ }
+
+ private void updatedZoom() {
+ if (hasPreviews()) {
+ for (RenderPreview preview : mPreviews) {
+ preview.disposeThumbnail();
+ }
+ RenderPreview preview = mCanvas.getPreview();
+ if (preview != null) {
+ preview.disposeThumbnail();
+ }
+ }
+
+ mNeedLayout = mNeedRender = true;
+ mCanvas.redraw();
+ }
+
+ static int getMaxWidth() {
+ return (int) (sScale * MAX_WIDTH);
+ }
+
+ static int getMaxHeight() {
+ return (int) (sScale * MAX_HEIGHT);
+ }
+
+ static double getScale() {
+ return sScale;
+ }
+
+ /**
+ * Returns whether there are any manual preview items (provided the current
+ * mode is manual previews
+ *
+ * @return true if there are items in the manual preview list
+ */
+ public boolean hasManualPreviews() {
+ assert mMode == CUSTOM;
+ return mManualList != null && !mManualList.isEmpty();
+ }
+
+ /** Delete all the previews */
+ public void deleteManualPreviews() {
+ disposePreviews();
+ selectMode(NONE);
+ mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/);
+
+ if (mManualList != null) {
+ mManualList.delete();
+ }
+ }
+
+ /** Dispose all the previews */
+ public void disposePreviews() {
+ if (mPreviews != null) {
+ List<RenderPreview> old = mPreviews;
+ mPreviews = null;
+ for (RenderPreview preview : old) {
+ preview.dispose();
+ }
+ }
+ }
+
+ /**
+ * Deletes the given preview
+ *
+ * @param preview the preview to be deleted
+ */
+ public void deletePreview(RenderPreview preview) {
+ mPreviews.remove(preview);
+ preview.dispose();
+ layout(true);
+ mCanvas.redraw();
+
+ if (mManualList != null) {
+ mManualList.remove(preview);
+ saveList();
+ }
+ }
+
+ /**
+ * Compute the total width required for the previews, including internal padding
+ *
+ * @return total width in pixels
+ */
+ public int computePreviewWidth() {
+ int maxPreviewWidth = 0;
+ if (hasPreviews()) {
+ for (RenderPreview preview : mPreviews) {
+ maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth());
+ }
+
+ if (maxPreviewWidth > 0) {
+ maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side
+ maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
+ }
+
+ return maxPreviewWidth;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Layout Algorithm. This sets the {@link RenderPreview#getX()} and
+ * {@link RenderPreview#getY()} coordinates of all the previews. It also
+ * marks previews as visible or invisible via
+ * {@link RenderPreview#setVisible(boolean)} according to their position and
+ * the current visible view port in the layout canvas. Finally, it also sets
+ * the {@code mLayoutHeight} field, such that the scrollbars can compute the
+ * right scrolled area, and that scrolling can cause render refreshes on
+ * views that are made visible.
+ * <p>
+ * This is not a traditional bin packing problem, because the objects to be
+ * packaged do not have a fixed size; we can scale them up and down in order
+ * to provide an "optimal" size.
+ * <p>
+ * See http://en.wikipedia.org/wiki/Packing_problem See
+ * http://en.wikipedia.org/wiki/Bin_packing_problem
+ */
+ void layout(boolean refresh) {
+ mNeedLayout = false;
+
+ if (mPreviews == null || mPreviews.isEmpty()) {
+ return;
+ }
+
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+
+ if (!refresh &&
+ (scaledImageWidth == mPrevImageWidth
+ && scaledImageHeight == mPrevImageHeight
+ && clientArea.width == mPrevCanvasWidth
+ && clientArea.height == mPrevCanvasHeight)) {
+ // No change
+ return;
+ }
+
+ mPrevImageWidth = scaledImageWidth;
+ mPrevImageHeight = scaledImageHeight;
+ mPrevCanvasWidth = clientArea.width;
+ mPrevCanvasHeight = clientArea.height;
+
+ if (mListener == null) {
+ mListener = new ScrollBarListener();
+ mCanvas.getVerticalBar().addSelectionListener(mListener);
+ }
+
+ beginRenderScheduling();
+
+ mLayoutHeight = 0;
+
+ if (previewsHaveIdenticalSize() || fixedOrder()) {
+ // If all the preview boxes are of identical sizes, or if the order is predetermined,
+ // just lay them out in rows.
+ rowLayout();
+ } else if (previewsFit()) {
+ layoutFullFit();
+ } else {
+ rowLayout();
+ }
+
+ mCanvas.updateScrollBars();
+ }
+
+ /**
+ * Performs a simple layout where the views are laid out in a row, wrapping
+ * around the top left canvas image.
+ */
+ private void rowLayout() {
+ // TODO: Separate layout heuristics for portrait and landscape orientations (though
+ // it also depends on the dimensions of the canvas window, which determines the
+ // shape of the leftover space)
+
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+
+ int availableWidth = clientArea.x + clientArea.width - getX();
+ int availableHeight = clientArea.y + clientArea.height - getY();
+ int maxVisibleY = clientArea.y + clientArea.height;
+
+ int bottomBorder = scaledImageHeight;
+ int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
+ int nextY = 0;
+
+ // First lay out images across the top right hand side
+ int x = rightHandSide;
+ int y = 0;
+ boolean wrapped = false;
+
+ int vgap = PREVIEW_VGAP;
+ for (RenderPreview preview : mPreviews) {
+ // If we have forked previews, double the vgap to allow space for two labels
+ if (preview.isForked()) {
+ vgap *= 2;
+ break;
+ }
+ }
+
+ List<RenderPreview> aspectOrder;
+ if (!fixedOrder()) {
+ aspectOrder = new ArrayList<RenderPreview>(mPreviews);
+ Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO);
+ } else {
+ aspectOrder = mPreviews;
+ }
+
+ for (RenderPreview preview : aspectOrder) {
+ if (x > 0 && x + preview.getWidth() > availableWidth) {
+ x = rightHandSide;
+ int prevY = y;
+ y = nextY;
+ if ((prevY <= bottomBorder ||
+ y <= bottomBorder)
+ && Math.max(nextY, y + preview.getHeight()) > bottomBorder) {
+ // If there's really no visible room below, don't bother
+ // Similarly, don't wrap individually scaled views
+ if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) {
+ // If it's closer to the top row than the bottom, just
+ // mark the next row for left justify instead
+ if (bottomBorder - y > y + preview.getHeight() - bottomBorder) {
+ rightHandSide = 0;
+ wrapped = true;
+ } else if (!wrapped) {
+ y = nextY = Math.max(nextY, bottomBorder + vgap);
+ x = rightHandSide = 0;
+ wrapped = true;
+ }
+ }
+ }
+ }
+ if (x > 0 && y <= bottomBorder
+ && Math.max(nextY, y + preview.getHeight()) > bottomBorder) {
+ if (clientArea.height - bottomBorder < preview.getHeight()) {
+ // No room below the device on the left; just continue on the
+ // bottom row
+ } else if (preview.getScale() < 1.2) {
+ if (bottomBorder - y > y + preview.getHeight() - bottomBorder) {
+ rightHandSide = 0;
+ wrapped = true;
+ } else {
+ y = nextY = Math.max(nextY, bottomBorder + vgap);
+ x = rightHandSide = 0;
+ wrapped = true;
+ }
+ }
+ }
+
+ preview.setPosition(x, y);
+
+ if (y > maxVisibleY && maxVisibleY > 0) {
+ preview.setVisible(false);
+ } else if (!preview.isVisible()) {
+ preview.setVisible(true);
+ }
+
+ x += preview.getWidth();
+ x += PREVIEW_HGAP;
+ nextY = Math.max(nextY, y + preview.getHeight() + vgap);
+ }
+
+ mLayoutHeight = nextY;
+ }
+
+ private boolean fixedOrder() {
+ return mMode == SCREENS;
+ }
+
+ /** Returns true if all the previews have the same identical size */
+ private boolean previewsHaveIdenticalSize() {
+ if (!hasPreviews()) {
+ return true;
+ }
+
+ Iterator<RenderPreview> iterator = mPreviews.iterator();
+ RenderPreview first = iterator.next();
+ int width = first.getWidth();
+ int height = first.getHeight();
+
+ while (iterator.hasNext()) {
+ RenderPreview preview = iterator.next();
+ if (width != preview.getWidth() || height != preview.getHeight()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns true if all the previews can fully fit in the available space */
+ private boolean previewsFit() {
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+ int availableWidth = clientArea.x + clientArea.width - getX();
+ int availableHeight = clientArea.y + clientArea.height - getY();
+ int bottomBorder = scaledImageHeight;
+ int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
+
+ // First see if we can fit everything; if so, we can try to make the layouts
+ // larger such that they fill up all the available space
+ long availableArea = rightHandSide * bottomBorder +
+ availableWidth * (Math.max(0, availableHeight - bottomBorder));
+
+ long requiredArea = 0;
+ for (RenderPreview preview : mPreviews) {
+ // Note: This does not include individual preview scale; the layout
+ // algorithm itself may be tweaking the scales to fit elements within
+ // the layout
+ requiredArea += preview.getArea();
+ }
+
+ return requiredArea * sScale < availableArea;
+ }
+
+ private void layoutFullFit() {
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+ int availableWidth = clientArea.x + clientArea.width - getX();
+ int availableHeight = clientArea.y + clientArea.height - getY();
+ int maxVisibleY = clientArea.y + clientArea.height;
+ int bottomBorder = scaledImageHeight;
+ int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
+
+ int minWidth = Integer.MAX_VALUE;
+ int minHeight = Integer.MAX_VALUE;
+ for (RenderPreview preview : mPreviews) {
+ minWidth = Math.min(minWidth, preview.getWidth());
+ minHeight = Math.min(minHeight, preview.getHeight());
+ }
+
+ BinPacker packer = new BinPacker(minWidth, minHeight);
+
+ // TODO: Instead of this, just start with client area and occupy scaled image size!
+
+ // Add in gap on right and bottom since we'll add that requirement on the width and
+ // height rectangles too (for spacing)
+ packer.addSpace(new Rect(rightHandSide, 0,
+ availableWidth - rightHandSide + PREVIEW_HGAP,
+ availableHeight + PREVIEW_VGAP));
+ if (maxVisibleY > bottomBorder) {
+ packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP,
+ availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP));
+ }
+
+ // TODO: Sort previews first before attempting to position them?
+
+ ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews);
+ Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO);
+
+ for (RenderPreview preview : aspectOrder) {
+ int previewWidth = preview.getWidth();
+ int previewHeight = preview.getHeight();
+ previewHeight += PREVIEW_VGAP;
+ if (preview.isForked()) {
+ previewHeight += PREVIEW_VGAP;
+ }
+ previewWidth += PREVIEW_HGAP;
+ // title height? how do I account for that?
+ Rect position = packer.occupy(previewWidth, previewHeight);
+ if (position != null) {
+ preview.setPosition(position.x, position.y);
+ preview.setVisible(true);
+ } else {
+ // Can't fit: give up and do plain row layout
+ rowLayout();
+ return;
+ }
+ }
+
+ mLayoutHeight = availableHeight;
+ }
+ /**
+ * Paints the configuration previews
+ *
+ * @param gc the graphics context to paint into
+ */
+ void paint(GC gc) {
+ if (hasPreviews()) {
+ // Ensure up to date at all times; consider moving if it's too expensive
+ layout(mNeedLayout);
+ if (mNeedRender) {
+ renderPreviews();
+ }
+ if (mNeedZoom) {
+ boolean allowZoomIn = true /*mMode == NONE*/;
+ mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn);
+ mNeedZoom = false;
+ }
+ int rootX = getX();
+ int rootY = getY();
+
+ for (RenderPreview preview : mPreviews) {
+ if (preview.isVisible()) {
+ int x = rootX + preview.getX();
+ int y = rootY + preview.getY();
+ preview.paint(gc, x, y);
+ }
+ }
+
+ RenderPreview preview = mCanvas.getPreview();
+ if (preview != null) {
+ String displayName = null;
+ Configuration configuration = preview.getConfiguration();
+ if (configuration instanceof VaryingConfiguration) {
+ // Use override flags from stashed preview, but configuration
+ // data from live (not varying) configured configuration
+ VaryingConfiguration cfg = (VaryingConfiguration) configuration;
+ int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags();
+ displayName = NestedConfiguration.computeDisplayName(flags,
+ getChooser().getConfiguration());
+ } else if (configuration instanceof NestedConfiguration) {
+ int flags = ((NestedConfiguration) configuration).getOverrideFlags();
+ displayName = NestedConfiguration.computeDisplayName(flags,
+ getChooser().getConfiguration());
+ } else {
+ displayName = configuration.getDisplayName();
+ }
+ if (displayName != null) {
+ CanvasTransform hi = mHScale;
+ CanvasTransform vi = mVScale;
+
+ int destX = hi.translate(0);
+ int destY = vi.translate(0);
+ int destWidth = hi.getScaledImgSize();
+ int destHeight = vi.getScaledImgSize();
+
+ int x = destX + destWidth / 2 - preview.getWidth() / 2;
+ int y = destY + destHeight;
+
+ preview.paintTitle(gc, x, y, false /*showFile*/, displayName);
+ }
+ }
+
+ // Zoom overlay
+ int x = getZoomX();
+ if (x > 0) {
+ int y = getZoomY();
+ int oldAlpha = gc.getAlpha();
+
+ // Paint background oval rectangle behind the zoom and close icons
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ gc.setAlpha(128);
+ int padding = 3;
+ int arc = 5;
+ gc.fillRoundRectangle(x - padding, y - padding,
+ ZOOM_ICON_WIDTH + 2 * padding,
+ 4 * ZOOM_ICON_HEIGHT + 2 * padding, arc, arc);
+
+ gc.setAlpha(255);
+ IconFactory iconFactory = IconFactory.getInstance();
+ Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$);
+ Image zoomIn = iconFactory.getIcon("zoomplus"); //$NON-NLS-1$);
+ Image zoom100 = iconFactory.getIcon("zoom100"); //$NON-NLS-1$);
+ Image close = iconFactory.getIcon("close"); //$NON-NLS-1$);
+
+ gc.drawImage(zoomIn, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.drawImage(zoomOut, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.drawImage(zoom100, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.drawImage(close, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.setAlpha(oldAlpha);
+ }
+ } else if (mMode == CUSTOM) {
+ int rootX = getX();
+ rootX += mHScale.getScaledImgSize();
+ rootX += 2 * PREVIEW_HGAP;
+ int rootY = getY();
+ rootY += 20;
+ gc.setFont(mCanvas.getFont());
+ gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK));
+ gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu",
+ rootX, rootY, true);
+ }
+
+ if (mAnimation != null) {
+ mAnimation.tick(gc);
+ }
+ }
+
+ private void addPreview(@NonNull RenderPreview preview) {
+ if (mPreviews == null) {
+ mPreviews = Lists.newArrayList();
+ }
+ mPreviews.add(preview);
+ }
+
+ /** Adds the current configuration as a new configuration preview */
+ public void addAsThumbnail() {
+ ConfigurationChooser chooser = getChooser();
+ String name = chooser.getConfiguration().getDisplayName();
+ if (name == null || name.isEmpty()) {
+ name = getUniqueName();
+ }
+ InputDialog d = new InputDialog(
+ AdtPlugin.getShell(),
+ "Add as Thumbnail Preview", // title
+ "Name of thumbnail:",
+ name,
+ null);
+ if (d.open() == Window.OK) {
+ selectMode(CUSTOM);
+
+ String newName = d.getValue();
+ // Create a new configuration from the current settings in the composite
+ Configuration configuration = Configuration.copy(chooser.getConfiguration());
+ configuration.setDisplayName(newName);
+
+ RenderPreview preview = RenderPreview.create(this, configuration);
+ addPreview(preview);
+
+ layout(true);
+ beginRenderScheduling();
+ scheduleRender(preview);
+ mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/);
+
+ if (mManualList == null) {
+ loadList();
+ }
+ if (mManualList != null) {
+ mManualList.add(preview);
+ saveList();
+ }
+ }
+ }
+
+ /**
+ * Computes a unique new name for a configuration preview that represents
+ * the current, default configuration
+ *
+ * @return a unique name
+ */
+ private String getUniqueName() {
+ if (mPreviews == null || mPreviews.isEmpty()) {
+ // NO, not for the first preview!
+ return "Config1";
+ }
+
+ Set<String> names = new HashSet<String>(mPreviews.size());
+ for (RenderPreview preview : mPreviews) {
+ names.add(preview.getDisplayName());
+ }
+
+ int index = 2;
+ while (true) {
+ String name = String.format("Config%1$d", index);
+ if (!names.contains(name)) {
+ return name;
+ }
+ index++;
+ }
+ }
+
+ /** Generates a bunch of default configuration preview thumbnails */
+ public void addDefaultPreviews() {
+ ConfigurationChooser chooser = getChooser();
+ Configuration parent = chooser.getConfiguration();
+ if (parent instanceof NestedConfiguration) {
+ parent = ((NestedConfiguration) parent).getParent();
+ }
+ if (mCanvas.getImageOverlay().getImage() != null) {
+ // Create Language variation
+ createLocaleVariation(chooser, parent);
+
+ // Vary screen size
+ // TODO: Be smarter here: Pick a screen that is both as differently as possible
+ // from the current screen as well as also supported. So consider
+ // things like supported screens, targetSdk etc.
+ createScreenVariations(parent);
+
+ // Vary orientation
+ createStateVariation(chooser, parent);
+
+ // Vary render target
+ createRenderTargetVariation(chooser, parent);
+ }
+
+ // Also add in include-context previews, if any
+ addIncludedInPreviews();
+
+ // Make a placeholder preview for the current screen, in case we switch from it
+ RenderPreview preview = RenderPreview.create(this, parent);
+ mCanvas.setPreview(preview);
+
+ sortPreviewsByOrientation();
+ }
+
+ private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) {
+ /* This is disabled for now: need to load multiple versions of layoutlib.
+ When I did this, there seemed to be some drug interactions between
+ them, and I would end up with NPEs in layoutlib code which normally works.
+ VaryingConfiguration configuration =
+ VaryingConfiguration.create(chooser, parent);
+ configuration.setAlternatingTarget(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ */
+ }
+
+ private void createStateVariation(ConfigurationChooser chooser, Configuration parent) {
+ State currentState = parent.getDeviceState();
+ State nextState = parent.getNextDeviceState(currentState);
+ if (nextState != currentState) {
+ VaryingConfiguration configuration =
+ VaryingConfiguration.create(chooser, parent);
+ configuration.setAlternateDeviceState(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ }
+ }
+
+ private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) {
+ LocaleQualifier currentLanguage = parent.getLocale().qualifier;
+ for (Locale locale : chooser.getLocaleList()) {
+ LocaleQualifier qualifier = locale.qualifier;
+ if (!qualifier.getLanguage().equals(currentLanguage.getLanguage())) {
+ VaryingConfiguration configuration =
+ VaryingConfiguration.create(chooser, parent);
+ configuration.setAlternateLocale(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ break;
+ }
+ }
+ }
+
+ private void createScreenVariations(Configuration parent) {
+ ConfigurationChooser chooser = getChooser();
+ VaryingConfiguration configuration;
+
+ configuration = VaryingConfiguration.create(chooser, parent);
+ configuration.setVariation(0);
+ configuration.setAlternateDevice(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+
+ configuration = VaryingConfiguration.create(chooser, parent);
+ configuration.setVariation(1);
+ configuration.setAlternateDevice(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ }
+
+ /**
+ * Returns the current mode as seen by this {@link RenderPreviewManager}.
+ * Note that it may not yet have been synced with the global mode kept in
+ * {@link AdtPrefs#getRenderPreviewMode()}.
+ *
+ * @return the current preview mode
+ */
+ @NonNull
+ public RenderPreviewMode getMode() {
+ return mMode;
+ }
+
+ /**
+ * Update the set of previews for the current mode
+ *
+ * @param force force a refresh even if the preview type has not changed
+ * @return true if the views were recomputed, false if the previews were
+ * already showing and the mode not changed
+ */
+ public boolean recomputePreviews(boolean force) {
+ RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode();
+ if (newMode == mMode && !force
+ && (mRevision == sRevision
+ || mMode == NONE
+ || mMode == CUSTOM)) {
+ return false;
+ }
+
+ RenderPreviewMode oldMode = mMode;
+ mMode = newMode;
+ mRevision = sRevision;
+
+ sScale = 1.0;
+ disposePreviews();
+
+ switch (mMode) {
+ case DEFAULT:
+ addDefaultPreviews();
+ break;
+ case INCLUDES:
+ addIncludedInPreviews();
+ break;
+ case LOCALES:
+ addLocalePreviews();
+ break;
+ case SCREENS:
+ addScreenSizePreviews();
+ break;
+ case VARIATIONS:
+ addVariationPreviews();
+ break;
+ case CUSTOM:
+ addManualPreviews();
+ break;
+ case NONE:
+ // Can't just set mNeedZoom because with no previews, the paint
+ // method does nothing
+ mCanvas.setFitScale(false /*onlyZoomOut*/, true /*allowZoomIn*/);
+ break;
+ default:
+ assert false : mMode;
+ }
+
+ // We schedule layout for the next redraw rather than process it here immediately;
+ // not only does this let us avoid doing work for windows where the tab is in the
+ // background, but when a file is opened we may not know the size of the canvas
+ // yet, and the layout methods need it in order to do a good job. By the time
+ // the canvas is painted, we have accurate bounds.
+ mNeedLayout = mNeedRender = true;
+ mCanvas.redraw();
+
+ if (oldMode != mMode && (oldMode == NONE || mMode == NONE)) {
+ // If entering or exiting preview mode: updating padding which is compressed
+ // only in preview mode.
+ mCanvas.getHorizontalTransform().refresh();
+ mCanvas.getVerticalTransform().refresh();
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets the new render preview mode to use
+ *
+ * @param mode the new mode
+ */
+ public void selectMode(@NonNull RenderPreviewMode mode) {
+ if (mode != mMode) {
+ AdtPrefs.getPrefs().setPreviewMode(mode);
+ recomputePreviews(false);
+ }
+ }
+
+ /** Similar to {@link #addDefaultPreviews()} but for locales */
+ public void addLocalePreviews() {
+
+ ConfigurationChooser chooser = getChooser();
+ List<Locale> locales = chooser.getLocaleList();
+ Configuration parent = chooser.getConfiguration();
+
+ for (Locale locale : locales) {
+ if (!locale.hasLanguage() && !locale.hasRegion()) {
+ continue;
+ }
+ NestedConfiguration configuration = NestedConfiguration.create(chooser, parent);
+ configuration.setOverrideLocale(true);
+ configuration.setLocale(locale, false);
+
+ String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
+ assert displayName != null; // it's never non null when locale is non null
+ configuration.setDisplayName(displayName);
+
+ addPreview(RenderPreview.create(this, configuration));
+ }
+
+ // Make a placeholder preview for the current screen, in case we switch from it
+ Configuration configuration = parent;
+ Locale locale = configuration.getLocale();
+ String label = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
+ if (label == null) {
+ label = "default";
+ }
+ configuration.setDisplayName(label);
+ RenderPreview preview = RenderPreview.create(this, parent);
+ if (preview != null) {
+ mCanvas.setPreview(preview);
+ }
+
+ // No need to sort: they should all be identical
+ }
+
+ /** Similar to {@link #addDefaultPreviews()} but for screen sizes */
+ public void addScreenSizePreviews() {
+ ConfigurationChooser chooser = getChooser();
+ Collection<Device> devices = chooser.getDevices();
+ Configuration configuration = chooser.getConfiguration();
+ boolean canScaleNinePatch = configuration.supports(Capability.FIXED_SCALABLE_NINE_PATCH);
+
+ // Rearrange the devices a bit such that the most interesting devices bubble
+ // to the front
+ // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first
+ // version of each seen screen size
+ List<Device> sorted = new ArrayList<Device>(devices);
+ Set<ScreenSize> seenSizes = new HashSet<ScreenSize>();
+ State currentState = configuration.getDeviceState();
+ String currentStateName = currentState != null ? currentState.getName() : "";
+
+ for (int i = 0, n = sorted.size(); i < n; i++) {
+ Device device = sorted.get(i);
+ boolean interesting = false;
+
+ State state = device.getState(currentStateName);
+ if (state == null) {
+ state = device.getAllStates().get(0);
+ }
+
+ if (device.getName().startsWith("Nexus ") //$NON-NLS-1$
+ || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$
+ // Not String#contains("Nexus") because that would also pick up all the generic
+ // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated
+ interesting = true;
+ }
+
+ FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state);
+ if (c != null) {
+ ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier();
+ if (sizeQualifier != null) {
+ ScreenSize size = sizeQualifier.getValue();
+ if (!seenSizes.contains(size)) {
+ seenSizes.add(size);
+ interesting = true;
+ }
+ }
+
+ // Omit LDPI, not really used anymore
+ DensityQualifier density = c.getDensityQualifier();
+ if (density != null) {
+ Density d = density.getValue();
+ if (d == Density.LOW) {
+ interesting = false;
+ }
+
+ if (!canScaleNinePatch && d == Density.TV) {
+ interesting = false;
+ }
+ }
+ }
+
+ if (interesting) {
+ NestedConfiguration screenConfig = NestedConfiguration.create(chooser,
+ configuration);
+ screenConfig.setOverrideDevice(true);
+ screenConfig.setDevice(device, true);
+ screenConfig.syncFolderConfig();
+ screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true));
+ addPreview(RenderPreview.create(this, screenConfig));
+ }
+ }
+
+ // Sorted by screen size, in decreasing order
+ sortPreviewsByScreenSize();
+ }
+
+ /**
+ * Previews this layout as included in other layouts
+ */
+ public void addIncludedInPreviews() {
+ ConfigurationChooser chooser = getChooser();
+ IProject project = chooser.getProject();
+ if (project == null) {
+ return;
+ }
+ IncludeFinder finder = IncludeFinder.get(project);
+
+ final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile());
+
+ if (includedBy == null || includedBy.isEmpty()) {
+ // TODO: Generate some useful defaults, such as including it in a ListView
+ // as the list item layout?
+ return;
+ }
+
+ for (final Reference reference : includedBy) {
+ String title = reference.getDisplayName();
+ Configuration config = Configuration.create(chooser.getConfiguration(),
+ reference.getFile());
+ RenderPreview preview = RenderPreview.create(this, config);
+ preview.setDisplayName(title);
+ preview.setIncludedWithin(reference);
+
+ addPreview(preview);
+ }
+
+ sortPreviewsByOrientation();
+ }
+
+ /**
+ * Previews this layout as included in other layouts
+ */
+ public void addVariationPreviews() {
+ ConfigurationChooser chooser = getChooser();
+
+ IFile file = chooser.getEditedFile();
+ List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/);
+
+ // Sort by parent folder
+ Collections.sort(variations, new Comparator<IFile>() {
+ @Override
+ public int compare(IFile file1, IFile file2) {
+ return file1.getParent().getName().compareTo(file2.getParent().getName());
+ }
+ });
+
+ Configuration currentConfig = chooser.getConfiguration();
+
+ for (IFile variation : variations) {
+ String title = variation.getParent().getName();
+ Configuration config = Configuration.create(chooser.getConfiguration(), variation);
+ config.setTheme(currentConfig.getTheme());
+ config.setActivity(currentConfig.getActivity());
+ RenderPreview preview = RenderPreview.create(this, config);
+ preview.setDisplayName(title);
+ preview.setAlternateInput(variation);
+
+ addPreview(preview);
+ }
+
+ sortPreviewsByOrientation();
+ }
+
+ /**
+ * Previews this layout using a custom configured set of layouts
+ */
+ public void addManualPreviews() {
+ if (mManualList == null) {
+ loadList();
+ } else {
+ mPreviews = mManualList.createPreviews(mCanvas);
+ }
+ }
+
+ private void loadList() {
+ IProject project = getChooser().getProject();
+ if (project == null) {
+ return;
+ }
+
+ if (mManualList == null) {
+ mManualList = RenderPreviewList.get(project);
+ }
+
+ try {
+ mManualList.load(getChooser().getDevices());
+ mPreviews = mManualList.createPreviews(mCanvas);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private void saveList() {
+ if (mManualList != null) {
+ try {
+ mManualList.save();
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ void rename(ConfigurationDescription description, String newName) {
+ IProject project = getChooser().getProject();
+ if (project == null) {
+ return;
+ }
+
+ if (mManualList == null) {
+ mManualList = RenderPreviewList.get(project);
+ }
+ description.displayName = newName;
+ saveList();
+ }
+
+
+ /**
+ * Notifies that the main configuration has changed.
+ *
+ * @param flags the change flags, a bitmask corresponding to the
+ * {@code CHANGE_} constants in {@link ConfigurationClient}
+ */
+ public void configurationChanged(int flags) {
+ // Similar to renderPreviews, but only acts on incomplete previews
+ if (hasPreviews()) {
+ // Do zoomed images first
+ beginRenderScheduling();
+ for (RenderPreview preview : mPreviews) {
+ if (preview.getScale() > 1.2) {
+ preview.configurationChanged(flags);
+ }
+ }
+ for (RenderPreview preview : mPreviews) {
+ if (preview.getScale() <= 1.2) {
+ preview.configurationChanged(flags);
+ }
+ }
+ RenderPreview preview = mCanvas.getPreview();
+ if (preview != null) {
+ preview.configurationChanged(flags);
+ preview.dispose();
+ }
+ mNeedLayout = true;
+ mCanvas.redraw();
+ }
+ }
+
+ /** Updates the configuration preview thumbnails */
+ public void renderPreviews() {
+ if (hasPreviews()) {
+ beginRenderScheduling();
+
+ // Process in visual order
+ ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews);
+ Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER);
+
+ // Do zoomed images first
+ for (RenderPreview preview : visualOrder) {
+ if (preview.getScale() > 1.2 && preview.isVisible()) {
+ scheduleRender(preview);
+ }
+ }
+ // Non-zoomed images
+ for (RenderPreview preview : visualOrder) {
+ if (preview.getScale() <= 1.2 && preview.isVisible()) {
+ scheduleRender(preview);
+ }
+ }
+ }
+
+ mNeedRender = false;
+ }
+
+ private int mPendingRenderCount;
+
+ /**
+ * Reset rendering scheduling. The next render request will be scheduled
+ * after a single delay unit.
+ */
+ public void beginRenderScheduling() {
+ mPendingRenderCount = 0;
+ }
+
+ /**
+ * Schedule rendering the given preview. Each successive call will add an additional
+ * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)}
+ * call, until {@link #beginRenderScheduling()} is called again.
+ *
+ * @param preview the preview to render
+ */
+ public void scheduleRender(@NonNull RenderPreview preview) {
+ mPendingRenderCount++;
+ preview.render(mPendingRenderCount * RENDER_DELAY);
+ }
+
+ /**
+ * Switch to the given configuration preview
+ *
+ * @param preview the preview to switch to
+ */
+ public void switchTo(@NonNull RenderPreview preview) {
+ IFile input = preview.getAlternateInput();
+ if (input != null) {
+ IWorkbenchPartSite site = mCanvas.getEditorDelegate().getEditor().getSite();
+ try {
+ // This switches to the given file, but the file might not have
+ // an identical configuration to what was shown in the preview.
+ // For example, while viewing a 10" layout-xlarge file, it might
+ // show a preview for a 5" version tied to the default layout. If
+ // you click on it, it will open the default layout file, but it might
+ // be using a different screen size; any of those that match the
+ // default layout, say a 3.8".
+ //
+ // Thus, we need to also perform a screen size sync first
+ Configuration configuration = preview.getConfiguration();
+ boolean setSize = false;
+ if (configuration instanceof NestedConfiguration) {
+ NestedConfiguration nestedConfig = (NestedConfiguration) configuration;
+ setSize = nestedConfig.isOverridingDevice();
+ if (configuration instanceof VaryingConfiguration) {
+ VaryingConfiguration c = (VaryingConfiguration) configuration;
+ setSize |= c.isAlternatingDevice();
+ }
+
+ if (setSize) {
+ ConfigurationChooser chooser = getChooser();
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile != null) {
+ chooser.syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE,
+ editedFile, configuration, false, false);
+ }
+ }
+ }
+
+ IDE.openEditor(site.getWorkbenchWindow().getActivePage(), input,
+ CommonXmlEditor.ID);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ return;
+ }
+
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ ConfigurationChooser chooser = editor.getConfigurationChooser();
+
+ Configuration originalConfiguration = chooser.getConfiguration();
+
+ // The new configuration is the configuration which will become the configuration
+ // in the layout editor's chooser
+ Configuration previewConfiguration = preview.getConfiguration();
+ Configuration newConfiguration = previewConfiguration;
+ if (newConfiguration instanceof NestedConfiguration) {
+ // Should never use a complementing configuration for the main
+ // rendering's configuration; instead, create a new configuration
+ // with a snapshot of the configuration's current values
+ newConfiguration = Configuration.copy(previewConfiguration);
+
+ // Remap all the previews to be parented to this new copy instead
+ // of the old one (which is no longer controlled by the chooser)
+ for (RenderPreview p : mPreviews) {
+ Configuration configuration = p.getConfiguration();
+ if (configuration instanceof NestedConfiguration) {
+ NestedConfiguration nested = (NestedConfiguration) configuration;
+ nested.setParent(newConfiguration);
+ }
+ }
+ }
+
+ // Make a preview for the configuration which *was* showing in the
+ // chooser up until this point:
+ RenderPreview newPreview = mCanvas.getPreview();
+ if (newPreview == null) {
+ newPreview = RenderPreview.create(this, originalConfiguration);
+ }
+
+ // Update its configuration such that it is complementing or inheriting
+ // from the new chosen configuration
+ if (previewConfiguration instanceof VaryingConfiguration) {
+ VaryingConfiguration varying = VaryingConfiguration.create(
+ (VaryingConfiguration) previewConfiguration,
+ newConfiguration);
+ varying.updateDisplayName();
+ originalConfiguration = varying;
+ newPreview.setConfiguration(originalConfiguration);
+ } else if (previewConfiguration instanceof NestedConfiguration) {
+ NestedConfiguration nested = NestedConfiguration.create(
+ (NestedConfiguration) previewConfiguration,
+ originalConfiguration,
+ newConfiguration);
+ nested.setDisplayName(nested.computeDisplayName());
+ originalConfiguration = nested;
+ newPreview.setConfiguration(originalConfiguration);
+ }
+
+ // Replace clicked preview with preview of the formerly edited main configuration
+ // This doesn't work yet because the image overlay has had its image
+ // replaced by the configuration previews! I should make a list of them
+ //newPreview.setFullImage(mImageOverlay.getAwtImage());
+ for (int i = 0, n = mPreviews.size(); i < n; i++) {
+ if (preview == mPreviews.get(i)) {
+ mPreviews.set(i, newPreview);
+ break;
+ }
+ }
+
+ // Stash the corresponding preview (not active) on the canvas so we can
+ // retrieve it if clicking to some other preview later
+ mCanvas.setPreview(preview);
+ preview.setVisible(false);
+
+ // Switch to the configuration from the clicked preview (though it's
+ // most likely a copy, see above)
+ chooser.setConfiguration(newConfiguration);
+ editor.changed(MASK_ALL);
+
+ // Scroll to the top again, if necessary
+ mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum());
+
+ mNeedLayout = mNeedZoom = true;
+ mCanvas.redraw();
+ mAnimation = new SwapAnimation(preview, newPreview);
+ }
+
+ /**
+ * Gets the preview at the given location, or null if none. This is
+ * currently deeply tied to where things are painted in onPaint().
+ */
+ RenderPreview getPreview(ControlPoint mousePos) {
+ if (hasPreviews()) {
+ int rootX = getX();
+ if (mousePos.x < rootX) {
+ return null;
+ }
+ int rootY = getY();
+
+ for (RenderPreview preview : mPreviews) {
+ int x = rootX + preview.getX();
+ int y = rootY + preview.getY();
+ if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) {
+ if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) {
+ return preview;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private int getX() {
+ return mHScale.translate(0);
+ }
+
+ private int getY() {
+ return mVScale.translate(0);
+ }
+
+ private int getZoomX() {
+ Rectangle clientArea = mCanvas.getClientArea();
+ int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH;
+ if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) {
+ // No visible previews because the main image is zoomed too far
+ return -1;
+ }
+
+ return x - 6;
+ }
+
+ private int getZoomY() {
+ Rectangle clientArea = mCanvas.getClientArea();
+ return clientArea.y + 5;
+ }
+
+ /**
+ * Returns the height of the layout
+ *
+ * @return the height
+ */
+ public int getHeight() {
+ return mLayoutHeight;
+ }
+
+ /**
+ * Notifies that preview manager that the mouse cursor has moved to the
+ * given control position within the layout canvas
+ *
+ * @param mousePos the mouse position, relative to the layout canvas
+ */
+ public void moved(ControlPoint mousePos) {
+ RenderPreview hovered = getPreview(mousePos);
+ if (hovered != mActivePreview) {
+ if (mActivePreview != null) {
+ mActivePreview.setActive(false);
+ }
+ mActivePreview = hovered;
+ if (mActivePreview != null) {
+ mActivePreview.setActive(true);
+ }
+ mCanvas.redraw();
+ }
+ }
+
+ /**
+ * Notifies that preview manager that the mouse cursor has entered the layout canvas
+ *
+ * @param mousePos the mouse position, relative to the layout canvas
+ */
+ public void enter(ControlPoint mousePos) {
+ moved(mousePos);
+ }
+
+ /**
+ * Notifies that preview manager that the mouse cursor has exited the layout canvas
+ *
+ * @param mousePos the mouse position, relative to the layout canvas
+ */
+ public void exit(ControlPoint mousePos) {
+ if (mActivePreview != null) {
+ mActivePreview.setActive(false);
+ }
+ mActivePreview = null;
+ mCanvas.redraw();
+ }
+
+ /**
+ * Process a mouse click, and return true if it was handled by this manager
+ * (e.g. the click was on a preview)
+ *
+ * @param mousePos the mouse position where the click occurred
+ * @return true if the click occurred over a preview and was handled, false otherwise
+ */
+ public boolean click(ControlPoint mousePos) {
+ // Clicked zoom?
+ int x = getZoomX();
+ if (x > 0) {
+ if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) {
+ int y = getZoomY();
+ if (mousePos.y >= y && mousePos.y <= y + 4 * ZOOM_ICON_HEIGHT) {
+ if (mousePos.y < y + ZOOM_ICON_HEIGHT) {
+ zoomIn();
+ } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) {
+ zoomOut();
+ } else if (mousePos.y < y + 3 * ZOOM_ICON_HEIGHT) {
+ zoomReset();
+ } else {
+ selectMode(NONE);
+ }
+ return true;
+ }
+ }
+ }
+
+ RenderPreview preview = getPreview(mousePos);
+ if (preview != null) {
+ boolean handled = preview.click(mousePos.x - getX() - preview.getX(),
+ mousePos.y - getY() - preview.getY());
+ if (handled) {
+ // In case layout was performed, there could be a new preview
+ // under this coordinate now, so make sure it's hover etc
+ // shows up
+ moved(mousePos);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if there are thumbnail previews
+ *
+ * @return true if thumbnails are being shown
+ */
+ public boolean hasPreviews() {
+ return mPreviews != null && !mPreviews.isEmpty();
+ }
+
+
+ private void sortPreviewsByScreenSize() {
+ if (mPreviews != null) {
+ Collections.sort(mPreviews, new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ Configuration config1 = preview1.getConfiguration();
+ Configuration config2 = preview2.getConfiguration();
+ Device device1 = config1.getDevice();
+ Device device2 = config1.getDevice();
+ if (device1 != null && device2 != null) {
+ Screen screen1 = device1.getDefaultHardware().getScreen();
+ Screen screen2 = device2.getDefaultHardware().getScreen();
+ if (screen1 != null && screen2 != null) {
+ double delta = screen1.getDiagonalLength()
+ - screen2.getDiagonalLength();
+ if (delta != 0.0) {
+ return (int) Math.signum(delta);
+ } else {
+ if (screen1.getPixelDensity() != screen2.getPixelDensity()) {
+ return screen1.getPixelDensity().compareTo(
+ screen2.getPixelDensity());
+ }
+ }
+ }
+
+ }
+ State state1 = config1.getDeviceState();
+ State state2 = config2.getDeviceState();
+ if (state1 != state2 && state1 != null && state2 != null) {
+ return state1.getName().compareTo(state2.getName());
+ }
+
+ return preview1.getDisplayName().compareTo(preview2.getDisplayName());
+ }
+ });
+ }
+ }
+
+ private void sortPreviewsByOrientation() {
+ if (mPreviews != null) {
+ Collections.sort(mPreviews, new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ Configuration config1 = preview1.getConfiguration();
+ Configuration config2 = preview2.getConfiguration();
+ State state1 = config1.getDeviceState();
+ State state2 = config2.getDeviceState();
+ if (state1 != state2 && state1 != null && state2 != null) {
+ return state1.getName().compareTo(state2.getName());
+ }
+
+ return preview1.getDisplayName().compareTo(preview2.getDisplayName());
+ }
+ });
+ }
+ }
+
+ /**
+ * Vertical scrollbar listener which updates render previews which are not
+ * visible and triggers a redraw
+ */
+ private class ScrollBarListener implements SelectionListener {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mPreviews == null) {
+ return;
+ }
+
+ ScrollBar bar = mCanvas.getVerticalBar();
+ int selection = bar.getSelection();
+ int thumb = bar.getThumb();
+ int maxY = selection + thumb;
+ beginRenderScheduling();
+ for (RenderPreview preview : mPreviews) {
+ if (!preview.isVisible() && preview.getY() <= maxY) {
+ preview.setVisible(true);
+ }
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+ }
+
+ /** Animation overlay shown briefly after swapping two previews */
+ private class SwapAnimation implements Runnable {
+ private long begin;
+ private long end;
+ private static final long DURATION = 400; // ms
+ private Rect initialRect1;
+ private Rect targetRect1;
+ private Rect initialRect2;
+ private Rect targetRect2;
+ private RenderPreview preview;
+
+ SwapAnimation(RenderPreview preview1, RenderPreview preview2) {
+ begin = System.currentTimeMillis();
+ end = begin + DURATION;
+
+ initialRect1 = new Rect(preview1.getX(), preview1.getY(),
+ preview1.getWidth(), preview1.getHeight());
+
+ CanvasTransform hi = mCanvas.getHorizontalTransform();
+ CanvasTransform vi = mCanvas.getVerticalTransform();
+ initialRect2 = new Rect(hi.translate(0), vi.translate(0),
+ hi.getScaledImgSize(), vi.getScaledImgSize());
+ preview = preview2;
+ }
+
+ void tick(GC gc) {
+ long now = System.currentTimeMillis();
+ if (now > end || mCanvas.isDisposed()) {
+ mAnimation = null;
+ return;
+ }
+
+ CanvasTransform hi = mCanvas.getHorizontalTransform();
+ CanvasTransform vi = mCanvas.getVerticalTransform();
+ if (targetRect1 == null) {
+ targetRect1 = new Rect(hi.translate(0), vi.translate(0),
+ hi.getScaledImgSize(), vi.getScaledImgSize());
+ }
+ double portion = (now - begin) / (double) DURATION;
+ Rect rect1 = new Rect(
+ (int) (portion * (targetRect1.x - initialRect1.x) + initialRect1.x),
+ (int) (portion * (targetRect1.y - initialRect1.y) + initialRect1.y),
+ (int) (portion * (targetRect1.w - initialRect1.w) + initialRect1.w),
+ (int) (portion * (targetRect1.h - initialRect1.h) + initialRect1.h));
+
+ if (targetRect2 == null) {
+ targetRect2 = new Rect(preview.getX(), preview.getY(),
+ preview.getWidth(), preview.getHeight());
+ }
+ portion = (now - begin) / (double) DURATION;
+ Rect rect2 = new Rect(
+ (int) (portion * (targetRect2.x - initialRect2.x) + initialRect2.x),
+ (int) (portion * (targetRect2.y - initialRect2.y) + initialRect2.y),
+ (int) (portion * (targetRect2.w - initialRect2.w) + initialRect2.w),
+ (int) (portion * (targetRect2.h - initialRect2.h) + initialRect2.h));
+
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ gc.drawRectangle(rect1.x, rect1.y, rect1.w, rect1.h);
+ gc.drawRectangle(rect2.x, rect2.y, rect2.w, rect2.h);
+
+ mCanvas.getDisplay().timerExec(5, this);
+ }
+
+ @Override
+ public void run() {
+ mCanvas.redraw();
+ }
+ }
+
+ /**
+ * Notifies the {@linkplain RenderPreviewManager} that the configuration used
+ * in the main chooser has been changed. This may require updating parent references
+ * in the preview configurations inheriting from it.
+ *
+ * @param oldConfiguration the previous configuration
+ * @param newConfiguration the new configuration in the chooser
+ */
+ public void updateChooserConfig(
+ @NonNull Configuration oldConfiguration,
+ @NonNull Configuration newConfiguration) {
+ if (hasPreviews()) {
+ for (RenderPreview preview : mPreviews) {
+ Configuration configuration = preview.getConfiguration();
+ if (configuration instanceof NestedConfiguration) {
+ NestedConfiguration nestedConfig = (NestedConfiguration) configuration;
+ if (nestedConfig.getParent() == oldConfiguration) {
+ nestedConfig.setParent(newConfiguration);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java
new file mode 100644
index 000000000..0f06d7f8a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+/**
+ * The {@linkplain RenderPreviewMode} records what type of configurations to
+ * render in the layout editor
+ */
+public enum RenderPreviewMode {
+ /** Generate a set of default previews with maximum variation */
+ DEFAULT,
+
+ /** Preview all the locales */
+ LOCALES,
+
+ /** Preview all the screen sizes */
+ SCREENS,
+
+ /** Preview layout as included in other layouts */
+ INCLUDES,
+
+ /** Preview all the variations of this layout */
+ VARIATIONS,
+
+ /** Show a manually configured set of previews */
+ CUSTOM,
+
+ /** No previews */
+ NONE;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
new file mode 100644
index 000000000..3b9e2fc0f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.HardwareConfigHelper;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.api.AssetRepository;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.DrawableParams;
+import com.android.ide.common.rendering.api.HardwareConfig;
+import com.android.ide.common.rendering.api.IImageFactory;
+import com.android.ide.common.rendering.api.ILayoutPullParser;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.eclipse.core.resources.IProject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.awt.Toolkit;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The {@link RenderService} provides rendering and layout information for
+ * Android layouts. This is a wrapper around the layout library.
+ */
+public class RenderService {
+ private static final Object RENDERING_LOCK = new Object();
+
+ /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
+ private final GraphicalEditorPart mEditor;
+
+ // The following fields are inferred from the editor and not customizable by the
+ // client of the render service:
+
+ private final IProject mProject;
+ private final ProjectCallback mProjectCallback;
+ private final ResourceResolver mResourceResolver;
+ private final int mMinSdkVersion;
+ private final int mTargetSdkVersion;
+ private final LayoutLibrary mLayoutLib;
+ private final IImageFactory mImageFactory;
+ private final HardwareConfigHelper mHardwareConfigHelper;
+ private final Locale mLocale;
+
+ // The following fields are optional or configurable using the various chained
+ // setters:
+
+ private UiDocumentNode mModel;
+ private Reference mIncludedWithin;
+ private RenderingMode mRenderingMode = RenderingMode.NORMAL;
+ private LayoutLog mLogger;
+ private Integer mOverrideBgColor;
+ private boolean mShowDecorations = true;
+ private Set<UiElementNode> mExpandNodes = Collections.<UiElementNode>emptySet();
+ private final Object mCredential;
+
+ /** Use the {@link #create} factory instead */
+ private RenderService(GraphicalEditorPart editor, Object credential) {
+ mEditor = editor;
+ mCredential = credential;
+
+ mProject = editor.getProject();
+ LayoutCanvas canvas = editor.getCanvasControl();
+ mImageFactory = canvas.getImageOverlay();
+ ConfigurationChooser chooser = editor.getConfigurationChooser();
+ Configuration config = chooser.getConfiguration();
+ FolderConfiguration folderConfig = config.getFullConfig();
+
+ Device device = config.getDevice();
+ assert device != null; // Should only attempt render with configuration that has device
+ mHardwareConfigHelper = new HardwareConfigHelper(device);
+ mHardwareConfigHelper.setOrientation(
+ folderConfig.getScreenOrientationQualifier().getValue());
+
+ mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
+ mResourceResolver = editor.getResourceResolver();
+ mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
+ mMinSdkVersion = editor.getMinSdkVersion();
+ mTargetSdkVersion = editor.getTargetSdkVersion();
+ mLocale = config.getLocale();
+ }
+
+ private RenderService(GraphicalEditorPart editor,
+ Configuration configuration, ResourceResolver resourceResolver,
+ Object credential) {
+ mEditor = editor;
+ mCredential = credential;
+
+ mProject = editor.getProject();
+ LayoutCanvas canvas = editor.getCanvasControl();
+ mImageFactory = canvas.getImageOverlay();
+ FolderConfiguration folderConfig = configuration.getFullConfig();
+
+ Device device = configuration.getDevice();
+ assert device != null;
+ mHardwareConfigHelper = new HardwareConfigHelper(device);
+ mHardwareConfigHelper.setOrientation(
+ folderConfig.getScreenOrientationQualifier().getValue());
+
+ mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
+ mResourceResolver = resourceResolver != null ? resourceResolver : editor.getResourceResolver();
+ mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
+ mMinSdkVersion = editor.getMinSdkVersion();
+ mTargetSdkVersion = editor.getTargetSdkVersion();
+ mLocale = configuration.getLocale();
+ }
+
+ private RenderSecurityManager createSecurityManager() {
+ String projectPath = null;
+ String sdkPath = null;
+ if (RenderSecurityManager.RESTRICT_READS) {
+ projectPath = AdtUtils.getAbsolutePath(mProject).toFile().getPath();
+ Sdk sdk = Sdk.getCurrent();
+ sdkPath = sdk != null ? sdk.getSdkOsLocation() : null;
+ }
+ RenderSecurityManager securityManager = new RenderSecurityManager(sdkPath, projectPath);
+ securityManager.setLogger(AdtPlugin.getDefault());
+
+ // Make sure this is initialized before we attempt to use it from layoutlib
+ Toolkit.getDefaultToolkit();
+
+ return securityManager;
+ }
+
+ /**
+ * Returns true if this configuration supports the given rendering
+ * capability
+ *
+ * @param target the target to look up the layout library for
+ * @param capability the capability to check
+ * @return true if the capability is supported
+ */
+ public static boolean supports(
+ @NonNull IAndroidTarget target,
+ @NonNull Capability capability) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AndroidTargetData targetData = sdk.getTargetData(target);
+ if (targetData != null) {
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ if (layoutLib != null) {
+ return layoutLib.supports(capability);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ public static RenderService create(GraphicalEditorPart editor) {
+ // Delegate to editor such that it can pass its credential to the service
+ return editor.createRenderService();
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @param credential the sandbox credential
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ @NonNull
+ public static RenderService create(GraphicalEditorPart editor, Object credential) {
+ return new RenderService(editor, credential);
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @param configuration the configuration to use (and fallback to editor for the rest)
+ * @param resolver a resource resolver to use to look up resources
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ public static RenderService create(GraphicalEditorPart editor,
+ Configuration configuration, ResourceResolver resolver) {
+ // Delegate to editor such that it can pass its credential to the service
+ return editor.createRenderService(configuration, resolver);
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @param configuration the configuration to use (and fallback to editor for the rest)
+ * @param resolver a resource resolver to use to look up resources
+ * @param credential the sandbox credential
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ public static RenderService create(GraphicalEditorPart editor,
+ Configuration configuration, ResourceResolver resolver, Object credential) {
+ return new RenderService(editor, configuration, resolver, credential);
+ }
+
+ /**
+ * Renders the given model, using this editor's theme and screen settings, and returns
+ * the result as a {@link RenderSession}.
+ *
+ * @param model the model to be rendered, which can be different than the editor's own
+ * {@link #getModel()}.
+ * @param width the width to use for the layout, or -1 to use the width of the screen
+ * associated with this editor
+ * @param height the height to use for the layout, or -1 to use the height of the screen
+ * associated with this editor
+ * @param explodeNodes a set of nodes to explode, or null for none
+ * @param overrideBgColor If non-null, use the given color as a background to render over
+ * rather than the normal background requested by the theme
+ * @param noDecor If true, don't draw window decorations like the system bar
+ * @param logger a logger where rendering errors are reported
+ * @param renderingMode the {@link RenderingMode} to use for rendering
+ * @return the resulting rendered image wrapped in an {@link RenderSession}
+ */
+
+ /**
+ * Sets the {@link LayoutLog} to be used during rendering. If none is specified, a
+ * silent logger will be used.
+ *
+ * @param logger the log to be used
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setLog(LayoutLog logger) {
+ mLogger = logger;
+ return this;
+ }
+
+ /**
+ * Sets the model to be rendered, which can be different than the editor's own
+ * {@link GraphicalEditorPart#getModel()}.
+ *
+ * @param model the model to be rendered
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setModel(UiDocumentNode model) {
+ mModel = model;
+ return this;
+ }
+
+ /**
+ * Overrides the width and height to be used during rendering (which might be adjusted if
+ * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
+ *
+ * A value of -1 will make the rendering use the normal width and height coming from the
+ * {@link Configuration#getDevice()} object.
+ *
+ * @param overrideRenderWidth the width in pixels of the layout to be rendered
+ * @param overrideRenderHeight the height in pixels of the layout to be rendered
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight) {
+ mHardwareConfigHelper.setOverrideRenderSize(overrideRenderWidth, overrideRenderHeight);
+ return this;
+ }
+
+ /**
+ * Sets the max width and height to be used during rendering (which might be adjusted if
+ * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
+ *
+ * A value of -1 will make the rendering use the normal width and height coming from the
+ * {@link Configuration#getDevice()} object.
+ *
+ * @param maxRenderWidth the max width in pixels of the layout to be rendered
+ * @param maxRenderHeight the max height in pixels of the layout to be rendered
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setMaxRenderSize(int maxRenderWidth, int maxRenderHeight) {
+ mHardwareConfigHelper.setMaxRenderSize(maxRenderWidth, maxRenderHeight);
+ return this;
+ }
+
+ /**
+ * Sets the {@link RenderingMode} to be used during rendering. If none is specified,
+ * the default is {@link RenderingMode#NORMAL}.
+ *
+ * @param renderingMode the rendering mode to be used
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setRenderingMode(RenderingMode renderingMode) {
+ mRenderingMode = renderingMode;
+ return this;
+ }
+
+ /**
+ * Sets the overriding background color to be used, if any. The color should be a
+ * bitmask of AARRGGBB. The default is null.
+ *
+ * @param overrideBgColor the overriding background color to be used in the rendering,
+ * in the form of a AARRGGBB bitmask, or null to use no custom background.
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setOverrideBgColor(Integer overrideBgColor) {
+ mOverrideBgColor = overrideBgColor;
+ return this;
+ }
+
+ /**
+ * Sets whether the rendering should include decorations such as a system bar, an
+ * application bar etc depending on the SDK target and theme. The default is true.
+ *
+ * @param showDecorations true if the rendering should include system bars etc.
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setDecorations(boolean showDecorations) {
+ mShowDecorations = showDecorations;
+ return this;
+ }
+
+ /**
+ * Sets the nodes to expand during rendering. These will be padded with approximately
+ * 20 pixels and also highlighted by the {@link EmptyViewsOverlay}. The default is an
+ * empty collection.
+ *
+ * @param nodesToExpand the nodes to be expanded
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setNodesToExpand(Set<UiElementNode> nodesToExpand) {
+ mExpandNodes = nodesToExpand;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Reference} to an outer layout that this layout should be rendered
+ * within. The outer layout <b>must</b> contain an include tag which points to this
+ * layout. The default is null.
+ *
+ * @param includedWithin a reference to an outer layout to render this layout within
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setIncludedWithin(Reference includedWithin) {
+ mIncludedWithin = includedWithin;
+ return this;
+ }
+
+ /** Initializes any remaining optional fields after all setters have been called */
+ private void finishConfiguration() {
+ if (mLogger == null) {
+ // Silent logging
+ mLogger = new LayoutLog();
+ }
+ }
+
+ /**
+ * Renders the model and returns the result as a {@link RenderSession}.
+ * @return the {@link RenderSession} resulting from rendering the current model
+ */
+ public RenderSession createRenderSession() {
+ assert mModel != null : "Incomplete service config";
+ finishConfiguration();
+
+ if (mResourceResolver == null) {
+ // Abort the rendering if the resources are not found.
+ return null;
+ }
+
+ HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
+
+ UiElementPullParser modelParser = new UiElementPullParser(mModel,
+ false, mExpandNodes, hardwareConfig.getDensity(), mProject);
+ ILayoutPullParser topParser = modelParser;
+
+ // Code to support editing included layout
+ // first reset the layout parser just in case.
+ mProjectCallback.setLayoutParser(null, null);
+
+ if (mIncludedWithin != null) {
+ // Outer layout name:
+ String contextLayoutName = mIncludedWithin.getName();
+
+ // Find the layout file.
+ ResourceValue contextLayout = mResourceResolver.findResValue(
+ LAYOUT_RESOURCE_PREFIX + contextLayoutName, false /* forceFrameworkOnly*/);
+ if (contextLayout != null) {
+ File layoutFile = new File(contextLayout.getValue());
+ if (layoutFile.isFile()) {
+ try {
+ // Get the name of the layout actually being edited, without the extension
+ // as it's what IXmlPullParser.getParser(String) will receive.
+ String queryLayoutName = mEditor.getLayoutResourceName();
+ mProjectCallback.setLayoutParser(queryLayoutName, modelParser);
+ topParser = new ContextPullParser(mProjectCallback, layoutFile);
+ topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ String xmlText = Files.toString(layoutFile, Charsets.UTF_8);
+ topParser.setInput(new StringReader(xmlText));
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (XmlPullParserException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+ }
+
+ SessionParams params = new SessionParams(
+ topParser,
+ mRenderingMode,
+ mProject /* projectKey */,
+ hardwareConfig,
+ mResourceResolver,
+ mProjectCallback,
+ mMinSdkVersion,
+ mTargetSdkVersion,
+ mLogger);
+
+ // Request margin and baseline information.
+ // TODO: Be smarter about setting this; start without it, and on the first request
+ // for an extended view info, re-render in the same session, and then set a flag
+ // which will cause this to create extended view info each time from then on in the
+ // same session
+ params.setExtendedViewInfoMode(true);
+
+ params.setLocale(mLocale.toLocaleId());
+ params.setAssetRepository(new AssetRepository());
+
+ ManifestInfo manifestInfo = ManifestInfo.get(mProject);
+ try {
+ params.setRtlSupport(manifestInfo.isRtlSupported());
+ } catch (Exception e) {
+ // ignore.
+ }
+ if (!mShowDecorations) {
+ params.setForceNoDecor();
+ } else {
+ try {
+ params.setAppLabel(manifestInfo.getApplicationLabel());
+ params.setAppIcon(manifestInfo.getApplicationIcon());
+ String activity = mEditor.getConfigurationChooser().getConfiguration().getActivity();
+ if (activity != null) {
+ ActivityAttributes info = manifestInfo.getActivityAttributes(activity);
+ if (info != null) {
+ if (info.getLabel() != null) {
+ params.setAppLabel(info.getLabel());
+ }
+ if (info.getIcon() != null) {
+ params.setAppIcon(info.getIcon());
+ }
+ }
+ }
+ } catch (Exception e) {
+ // ignore.
+ }
+ }
+
+ if (mOverrideBgColor != null) {
+ params.setOverrideBgColor(mOverrideBgColor.intValue());
+ }
+
+ // set the Image Overlay as the image factory.
+ params.setImageFactory(mImageFactory);
+
+ mProjectCallback.setLogger(mLogger);
+ mProjectCallback.setResourceResolver(mResourceResolver);
+ RenderSecurityManager securityManager = createSecurityManager();
+ try {
+ securityManager.setActive(true, mCredential);
+ synchronized (RENDERING_LOCK) {
+ return mLayoutLib.createSession(params);
+ }
+ } catch (RuntimeException t) {
+ // Exceptions from the bridge
+ mLogger.error(null, t.getLocalizedMessage(), t, null);
+ throw t;
+ } finally {
+ securityManager.dispose(mCredential);
+ mProjectCallback.setLogger(null);
+ mProjectCallback.setResourceResolver(null);
+ }
+ }
+
+ /**
+ * Renders the given resource value (which should refer to a drawable) and returns it
+ * as an image
+ *
+ * @param drawableResourceValue the drawable resource value to be rendered, or null
+ * @return the image, or null if something went wrong
+ */
+ public BufferedImage renderDrawable(ResourceValue drawableResourceValue) {
+ if (drawableResourceValue == null) {
+ return null;
+ }
+
+ finishConfiguration();
+
+ HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
+
+ DrawableParams params = new DrawableParams(drawableResourceValue, mProject, hardwareConfig,
+ mResourceResolver, mProjectCallback, mMinSdkVersion,
+ mTargetSdkVersion, mLogger);
+ params.setAssetRepository(new AssetRepository());
+ params.setForceNoDecor();
+ Result result = mLayoutLib.renderDrawable(params);
+ if (result != null && result.isSuccess()) {
+ Object data = result.getData();
+ if (data instanceof BufferedImage) {
+ return (BufferedImage) data;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Measure the children of the given parent node, applying the given filter to the
+ * pull parser's attribute values.
+ *
+ * @param parent the parent node to measure children for
+ * @param filter the filter to apply to the attribute values
+ * @return a map from node children of the parent to new bounds of the nodes
+ */
+ public Map<INode, Rect> measureChildren(INode parent,
+ final IClientRulesEngine.AttributeFilter filter) {
+ finishConfiguration();
+ HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
+
+ final NodeFactory mNodeFactory = mEditor.getCanvasControl().getNodeFactory();
+ UiElementNode parentNode = ((NodeProxy) parent).getNode();
+ UiElementPullParser topParser = new UiElementPullParser(parentNode,
+ false, Collections.<UiElementNode>emptySet(), hardwareConfig.getDensity(),
+ mProject) {
+ @Override
+ public String getAttributeValue(String namespace, String localName) {
+ if (filter != null) {
+ Object cookie = getViewCookie();
+ if (cookie instanceof UiViewElementNode) {
+ NodeProxy node = mNodeFactory.create((UiViewElementNode) cookie);
+ if (node != null) {
+ String value = filter.getAttribute(node, namespace, localName);
+ if (value != null) {
+ return value;
+ }
+ // null means no preference, not "unset".
+ }
+ }
+ }
+
+ return super.getAttributeValue(namespace, localName);
+ }
+
+ /**
+ * The parser usually assumes that the top level node is a document node that
+ * should be skipped, and that's not the case when we render in the middle of
+ * the tree, so override {@link UiElementPullParser#onNextFromStartDocument}
+ * to change this behavior
+ */
+ @Override
+ public void onNextFromStartDocument() {
+ mParsingState = START_TAG;
+ }
+ };
+
+ SessionParams params = new SessionParams(
+ topParser,
+ RenderingMode.FULL_EXPAND,
+ mProject /* projectKey */,
+ hardwareConfig,
+ mResourceResolver,
+ mProjectCallback,
+ mMinSdkVersion,
+ mTargetSdkVersion,
+ mLogger);
+ params.setLayoutOnly();
+ params.setForceNoDecor();
+ params.setAssetRepository(new AssetRepository());
+
+ RenderSession session = null;
+ mProjectCallback.setLogger(mLogger);
+ mProjectCallback.setResourceResolver(mResourceResolver);
+ RenderSecurityManager securityManager = createSecurityManager();
+ try {
+ securityManager.setActive(true, mCredential);
+ synchronized (RENDERING_LOCK) {
+ session = mLayoutLib.createSession(params);
+ }
+ if (session.getResult().isSuccess()) {
+ assert session.getRootViews().size() == 1;
+ ViewInfo root = session.getRootViews().get(0);
+ List<ViewInfo> children = root.getChildren();
+ Map<INode, Rect> map = new HashMap<INode, Rect>(children.size());
+ for (ViewInfo info : children) {
+ if (info.getCookie() instanceof UiViewElementNode) {
+ UiViewElementNode uiNode = (UiViewElementNode) info.getCookie();
+ NodeProxy node = mNodeFactory.create(uiNode);
+ map.put(node, new Rect(info.getLeft(), info.getTop(),
+ info.getRight() - info.getLeft(),
+ info.getBottom() - info.getTop()));
+ }
+ }
+
+ return map;
+ }
+ } catch (RuntimeException t) {
+ // Exceptions from the bridge
+ mLogger.error(null, t.getLocalizedMessage(), t, null);
+ throw t;
+ } finally {
+ securityManager.dispose(mCredential);
+ mProjectCallback.setLogger(null);
+ mProjectCallback.setResourceResolver(null);
+ if (session != null) {
+ session.dispose();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java
new file mode 100644
index 000000000..4d51c07de
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.graphics.GC;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link ResizeGesture} is a gesture for resizing a selected widget. It is initiated
+ * by a drag of a {@link SelectionHandle}.
+ */
+public class ResizeGesture extends Gesture {
+ /** The {@link Overlay} drawn for the gesture feedback. */
+ private ResizeOverlay mOverlay;
+
+ /** The canvas associated with this gesture. */
+ private LayoutCanvas mCanvas;
+
+ /** The selection handle we're dragging to perform this resize */
+ private SelectionHandle mHandle;
+
+ private NodeProxy mParentNode;
+ private NodeProxy mChildNode;
+ private DropFeedback mFeedback;
+ private ResizePolicy mResizePolicy;
+ private SegmentType mHorizontalEdge;
+ private SegmentType mVerticalEdge;
+
+ /**
+ * Creates a new marquee selection (selection swiping).
+ *
+ * @param canvas The canvas where selection is performed.
+ * @param item The selected item the handle corresponds to
+ * @param handle The handle being dragged to perform the resize
+ */
+ public ResizeGesture(LayoutCanvas canvas, SelectionItem item, SelectionHandle handle) {
+ mCanvas = canvas;
+ mHandle = handle;
+
+ mChildNode = item.getNode();
+ mParentNode = (NodeProxy) mChildNode.getParent();
+ mResizePolicy = item.getResizePolicy();
+ mHorizontalEdge = getHorizontalEdgeType(mHandle);
+ mVerticalEdge = getVerticalEdgeType(mHandle);
+ }
+
+ @Override
+ public void begin(ControlPoint pos, int startMask) {
+ super.begin(pos, startMask);
+
+ mCanvas.getSelectionOverlay().setHidden(true);
+
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ Rect newBounds = getNewBounds(pos);
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ CanvasViewInfo childInfo = viewHierarchy.findViewInfoFor(mChildNode);
+ CanvasViewInfo parentInfo = viewHierarchy.findViewInfoFor(mParentNode);
+ Object childView = childInfo != null ? childInfo.getViewObject() : null;
+ Object parentView = parentInfo != null ? parentInfo.getViewObject() : null;
+ mFeedback = rulesEngine.callOnResizeBegin(mChildNode, mParentNode, newBounds,
+ mHorizontalEdge, mVerticalEdge, childView, parentView);
+ update(pos);
+ mCanvas.getGestureManager().updateMessage(mFeedback);
+ }
+
+ @Override
+ public boolean keyPressed(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+
+ @Override
+ public boolean keyReleased(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+
+ @Override
+ public void update(ControlPoint pos) {
+ super.update(pos);
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ Rect newBounds = getNewBounds(pos);
+ int modifierMask = mCanvas.getGestureManager().getRuleModifierMask();
+ rulesEngine.callOnResizeUpdate(mFeedback, mChildNode, mParentNode, newBounds,
+ modifierMask);
+ mCanvas.getGestureManager().updateMessage(mFeedback);
+ }
+
+ @Override
+ public void end(ControlPoint pos, boolean canceled) {
+ super.end(pos, canceled);
+
+ if (!canceled) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ Rect newBounds = getNewBounds(pos);
+ rulesEngine.callOnResizeEnd(mFeedback, mChildNode, mParentNode, newBounds);
+ }
+
+ mCanvas.getSelectionOverlay().setHidden(false);
+ }
+
+ @Override
+ public Pair<Boolean, Boolean> getTooltipPosition() {
+ return Pair.of(mHorizontalEdge != SegmentType.TOP, mVerticalEdge != SegmentType.LEFT);
+ }
+
+ /**
+ * For the new mouse position, compute the resized bounds (the bounding rectangle that
+ * the view should be resized to). This is not just a width or height, since in some
+ * cases resizing will change the x/y position of the view as well (for example, in
+ * RelativeLayout or in AbsoluteLayout).
+ */
+ private Rect getNewBounds(ControlPoint pos) {
+ LayoutPoint p = pos.toLayout();
+ LayoutPoint start = mStart.toLayout();
+ Rect b = mChildNode.getBounds();
+ Position direction = mHandle.getPosition();
+
+ int x = b.x;
+ int y = b.y;
+ int w = b.w;
+ int h = b.h;
+ int deltaX = p.x - start.x;
+ int deltaY = p.y - start.y;
+
+ if (deltaX == 0 && deltaY == 0) {
+ // No move - just use the existing bounds
+ return b;
+ }
+
+ if (mResizePolicy.isAspectPreserving() && w != 0 && h != 0) {
+ double aspectRatio = w / (double) h;
+ int newW = Math.abs(b.w + (direction.isLeft() ? -deltaX : deltaX));
+ int newH = Math.abs(b.h + (direction.isTop() ? -deltaY : deltaY));
+ double newAspectRatio = newW / (double) newH;
+ if (newH == 0 || newAspectRatio > aspectRatio) {
+ deltaY = (int) (deltaX / aspectRatio);
+ } else {
+ deltaX = (int) (deltaY * aspectRatio);
+ }
+ }
+ if (direction.isLeft()) {
+ // The user is dragging the left edge, so the position is anchored on the
+ // right.
+ int x2 = b.x + b.w;
+ int nx1 = b.x + deltaX;
+ if (nx1 <= x2) {
+ x = nx1;
+ w = x2 - x;
+ } else {
+ w = 0;
+ x = x2;
+ }
+ } else if (direction.isRight()) {
+ // The user is dragging the right edge, so the position is anchored on the
+ // left.
+ int nx2 = b.x + b.w + deltaX;
+ if (nx2 >= b.x) {
+ w = nx2 - b.x;
+ } else {
+ w = 0;
+ }
+ } else {
+ assert direction == Position.BOTTOM_MIDDLE || direction == Position.TOP_MIDDLE;
+ }
+
+ if (direction.isTop()) {
+ // The user is dragging the top edge, so the position is anchored on the
+ // bottom.
+ int y2 = b.y + b.h;
+ int ny1 = b.y + deltaY;
+ if (ny1 < y2) {
+ y = ny1;
+ h = y2 - y;
+ } else {
+ h = 0;
+ y = y2;
+ }
+ } else if (direction.isBottom()) {
+ // The user is dragging the bottom edge, so the position is anchored on the
+ // top.
+ int ny2 = b.y + b.h + deltaY;
+ if (ny2 >= b.y) {
+ h = ny2 - b.y;
+ } else {
+ h = 0;
+ }
+ } else {
+ assert direction == Position.LEFT_MIDDLE || direction == Position.RIGHT_MIDDLE;
+ }
+
+ return new Rect(x, y, w, h);
+ }
+
+ private static SegmentType getHorizontalEdgeType(SelectionHandle handle) {
+ switch (handle.getPosition()) {
+ case BOTTOM_LEFT:
+ case BOTTOM_RIGHT:
+ case BOTTOM_MIDDLE:
+ return SegmentType.BOTTOM;
+ case LEFT_MIDDLE:
+ case RIGHT_MIDDLE:
+ return null;
+ case TOP_LEFT:
+ case TOP_MIDDLE:
+ case TOP_RIGHT:
+ return SegmentType.TOP;
+ default: assert false : handle.getPosition();
+ }
+ return null;
+ }
+
+ private static SegmentType getVerticalEdgeType(SelectionHandle handle) {
+ switch (handle.getPosition()) {
+ case TOP_LEFT:
+ case LEFT_MIDDLE:
+ case BOTTOM_LEFT:
+ return SegmentType.LEFT;
+ case BOTTOM_MIDDLE:
+ case TOP_MIDDLE:
+ return null;
+ case TOP_RIGHT:
+ case RIGHT_MIDDLE:
+ case BOTTOM_RIGHT:
+ return SegmentType.RIGHT;
+ default: assert false : handle.getPosition();
+ }
+ return null;
+ }
+
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new ResizeOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ /**
+ * An {@link Overlay} to paint the resize feedback. This just delegates to the
+ * layout rule for the parent which is handling the resizing.
+ */
+ private class ResizeOverlay extends Overlay {
+ @Override
+ public void paint(GC gc) {
+ if (mChildNode != null && mFeedback != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mChildNode, mFeedback);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java
new file mode 100644
index 000000000..c2db2431c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+
+/**
+ * A selection handle is a small rectangle on the border of a selected view which lets you
+ * change the size of the view by dragging it.
+ */
+public class SelectionHandle {
+ /**
+ * Size of the selection handle radius, in control coordinates. Note that this isn't
+ * necessarily a <b>circular</b> radius; in the case of a rectangular handle, the
+ * width and the height are both equal to this radius.
+ * Note also that this radius is in <b>control</b> coordinates, whereas the rest
+ * of the class operates in layout coordinates. This is because we do not want the
+ * selection handles to grow or shrink along with the screen zoom; they are always
+ * at the given pixel size in the control.
+ */
+ public final static int PIXEL_RADIUS = 3;
+
+ /**
+ * Extra number of pixels to look beyond the actual radius of the selection handle
+ * when matching mouse positions to handles
+ */
+ public final static int PIXEL_MARGIN = 2;
+
+ /** The position of the handle in the selection rectangle */
+ enum Position {
+ TOP_MIDDLE(SWT.CURSOR_SIZEN),
+ TOP_RIGHT(SWT.CURSOR_SIZENE),
+ RIGHT_MIDDLE(SWT.CURSOR_SIZEE),
+ BOTTOM_RIGHT(SWT.CURSOR_SIZESE),
+ BOTTOM_MIDDLE(SWT.CURSOR_SIZES),
+ BOTTOM_LEFT(SWT.CURSOR_SIZESW),
+ LEFT_MIDDLE(SWT.CURSOR_SIZEW),
+ TOP_LEFT(SWT.CURSOR_SIZENW);
+
+ /** Corresponding SWT cursor value */
+ private int mSwtCursor;
+
+ private Position(int swtCursor) {
+ mSwtCursor = swtCursor;
+ }
+
+ private int getCursorType() {
+ return mSwtCursor;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the left edge? */
+ boolean isLeft() {
+ return this == TOP_LEFT || this == LEFT_MIDDLE || this == BOTTOM_LEFT;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the right edge? */
+ boolean isRight() {
+ return this == TOP_RIGHT || this == RIGHT_MIDDLE || this == BOTTOM_RIGHT;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the top edge? */
+ boolean isTop() {
+ return this == TOP_LEFT || this == TOP_MIDDLE || this == TOP_RIGHT;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the bottom edge? */
+ boolean isBottom() {
+ return this == BOTTOM_LEFT || this == BOTTOM_MIDDLE || this == BOTTOM_RIGHT;
+ }
+ };
+
+ /** The x coordinate of the center of the selection handle */
+ public final int centerX;
+ /** The y coordinate of the center of the selection handle */
+ public final int centerY;
+ /** The position of the handle in the selection rectangle */
+ private final Position mPosition;
+
+ /**
+ * Constructs a new {@link SelectionHandle} at the given layout coordinate
+ * corresponding to a handle at the given {@link Position}.
+ *
+ * @param centerX the x coordinate of the center of the selection handle
+ * @param centerY y coordinate of the center of the selection handle
+ * @param position the position of the handle in the selection rectangle
+ */
+ public SelectionHandle(int centerX, int centerY, Position position) {
+ mPosition = position;
+ this.centerX = centerX;
+ this.centerY = centerY;
+ }
+
+ /**
+ * Determines whether the given {@link LayoutPoint} is within the given distance in
+ * layout coordinates. The distance should incorporate at least the equivalent
+ * distance to the control coordinate space {@link #PIXEL_RADIUS}, but usually with a
+ * few extra pixels added in to make the corners easier to target.
+ *
+ * @param point the mouse position in layout coordinates
+ * @param distance the distance from the center of the handle to check whether the
+ * point fits within
+ * @return true if the given point is within the given distance of this handle
+ */
+ public boolean contains(LayoutPoint point, int distance) {
+ return (point.x >= centerX - distance
+ && point.x <= centerX + distance
+ && point.y >= centerY - distance
+ && point.y <= centerY + distance);
+ }
+
+ /**
+ * Returns the position of the handle in the selection rectangle
+ *
+ * @return the position of the handle in the selection rectangle
+ */
+ public Position getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Returns the SWT cursor type to use for this selection handle
+ *
+ * @return the position of the handle in the selection rectangle
+ */
+ public int getSwtCursorType() {
+ return mPosition.getCursorType();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java
new file mode 100644
index 000000000..6d7f34a66
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The {@link SelectionHandles} of a {@link SelectionItem} are the set of
+ * {@link SelectionHandle} objects (possibly empty, for non-resizable objects) the user
+ * can manipulate to resize a widget.
+ */
+public class SelectionHandles implements Iterable<SelectionHandle> {
+ private final SelectionItem mItem;
+ private List<SelectionHandle> mHandles;
+
+ /**
+ * Constructs a new {@link SelectionHandles} object for the given {link
+ * {@link SelectionItem}
+ * @param item the item to create {@link SelectionHandles} for
+ */
+ public SelectionHandles(SelectionItem item) {
+ mItem = item;
+
+ createHandles(item.getCanvas());
+ }
+
+ /**
+ * Find a specific {@link SelectionHandle} from this set of {@link SelectionHandles},
+ * which is within the given distance (in layout coordinates) from the center of the
+ * {@link SelectionHandle}.
+ *
+ * @param point the mouse position (in layout coordinates) to test
+ * @param distance the maximum distance from the handle center to accept
+ * @return a {@link SelectionHandle} under the point, or null if not found
+ */
+ public SelectionHandle findHandle(LayoutPoint point, int distance) {
+ for (SelectionHandle handle : mHandles) {
+ if (handle.contains(point, distance)) {
+ return handle;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Create the {@link SelectionHandle} objects for the selection item, according to its
+ * {@link ResizePolicy}.
+ */
+ private void createHandles(LayoutCanvas canvas) {
+ NodeProxy selectedNode = mItem.getNode();
+ Rect r = selectedNode.getBounds();
+ if (!r.isValid()) {
+ mHandles = Collections.emptyList();
+ return;
+ }
+
+ ResizePolicy resizability = mItem.getResizePolicy();
+ if (resizability.isResizable()) {
+ mHandles = new ArrayList<SelectionHandle>(8);
+ boolean left = resizability.leftAllowed();
+ boolean right = resizability.rightAllowed();
+ boolean top = resizability.topAllowed();
+ boolean bottom = resizability.bottomAllowed();
+ int x1 = r.x;
+ int y1 = r.y;
+ int w = r.w;
+ int h = r.h;
+ int x2 = x1 + w;
+ int y2 = y1 + h;
+
+ Margins insets = canvas.getInsets(mItem.getNode().getFqcn());
+ if (insets != null) {
+ x1 += insets.left;
+ x2 -= insets.right;
+ y1 += insets.top;
+ y2 -= insets.bottom;
+ }
+
+ int mx = (x1 + x2) / 2;
+ int my = (y1 + y2) / 2;
+
+ if (left) {
+ mHandles.add(new SelectionHandle(x1, my, Position.LEFT_MIDDLE));
+ if (top) {
+ mHandles.add(new SelectionHandle(x1, y1, Position.TOP_LEFT));
+ }
+ if (bottom) {
+ mHandles.add(new SelectionHandle(x1, y2, Position.BOTTOM_LEFT));
+ }
+ }
+ if (right) {
+ mHandles.add(new SelectionHandle(x2, my, Position.RIGHT_MIDDLE));
+ if (top) {
+ mHandles.add(new SelectionHandle(x2, y1, Position.TOP_RIGHT));
+ }
+ if (bottom) {
+ mHandles.add(new SelectionHandle(x2, y2, Position.BOTTOM_RIGHT));
+ }
+ }
+ if (top) {
+ mHandles.add(new SelectionHandle(mx, y1, Position.TOP_MIDDLE));
+ }
+ if (bottom) {
+ mHandles.add(new SelectionHandle(mx, y2, Position.BOTTOM_MIDDLE));
+ }
+ } else {
+ mHandles = Collections.emptyList();
+ }
+ }
+
+ // Implements Iterable<SelectionHandle>
+ @Override
+ public Iterator<SelectionHandle> iterator() {
+ return mHandles.iterator();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
new file mode 100644
index 000000000..d104e379e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents one selection in {@link LayoutCanvas}.
+ */
+class SelectionItem {
+
+ /** The associated {@link LayoutCanvas} */
+ private LayoutCanvas mCanvas;
+
+ /** Current selected view info. Can be null. */
+ private final CanvasViewInfo mCanvasViewInfo;
+
+ /** Current selection border rectangle. Null when mCanvasViewInfo is null . */
+ private final Rectangle mRect;
+
+ /** The node proxy for drawing the selection. Null when mCanvasViewInfo is null. */
+ private final NodeProxy mNodeProxy;
+
+ /** The resize policy for this selection item */
+ private ResizePolicy mResizePolicy;
+
+ /** The selection handles for this item */
+ private SelectionHandles mHandles;
+
+ /**
+ * Creates a new {@link SelectionItem} object.
+ * @param canvas the associated canvas
+ * @param canvasViewInfo The view info being selected. Must not be null.
+ */
+ public SelectionItem(LayoutCanvas canvas, CanvasViewInfo canvasViewInfo) {
+ assert canvasViewInfo != null;
+
+ mCanvas = canvas;
+ mCanvasViewInfo = canvasViewInfo;
+
+ if (canvasViewInfo == null) {
+ mRect = null;
+ mNodeProxy = null;
+ } else {
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ mRect = new Rectangle(r.x, r.y, r.width, r.height);
+ mNodeProxy = mCanvas.getNodeFactory().create(canvasViewInfo);
+ }
+ }
+
+ /**
+ * Returns true when this selection item represents the root, the top level
+ * layout element in the editor.
+ *
+ * @return True if and only if this element is at the root of the hierarchy
+ */
+ public boolean isRoot() {
+ return mCanvasViewInfo.isRoot();
+ }
+
+ /**
+ * Returns true if this item represents a widget that should not be manipulated by the
+ * user.
+ *
+ * @return True if this widget should not be manipulated directly by the user
+ */
+ public boolean isHidden() {
+ return mCanvasViewInfo.isHidden();
+ }
+
+ /**
+ * Returns the selected view info. Cannot be null.
+ *
+ * @return the selected view info. Cannot be null.
+ */
+ @NonNull
+ public CanvasViewInfo getViewInfo() {
+ return mCanvasViewInfo;
+ }
+
+ /**
+ * Returns the selected node.
+ *
+ * @return the selected node, or null
+ */
+ @Nullable
+ public UiViewElementNode getUiNode() {
+ return mCanvasViewInfo.getUiViewNode();
+ }
+
+ /**
+ * Returns the selection border rectangle. Cannot be null.
+ *
+ * @return the selection border rectangle, never null
+ */
+ public Rectangle getRect() {
+ return mRect;
+ }
+
+ /** Returns the node associated with this selection (may be null) */
+ @Nullable
+ NodeProxy getNode() {
+ return mNodeProxy;
+ }
+
+ /** Returns the canvas associated with this selection (never null) */
+ @NonNull
+ LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ //----
+
+ /**
+ * Gets the XML text from the given selection for a text transfer.
+ * The returned string can be empty but not null.
+ */
+ @NonNull
+ static String getAsText(LayoutCanvas canvas, List<SelectionItem> selection) {
+ StringBuilder sb = new StringBuilder();
+
+ LayoutEditorDelegate layoutEditorDelegate = canvas.getEditorDelegate();
+ for (SelectionItem cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi.getUiViewNode();
+ Node node = key.getXmlNode();
+ String t = layoutEditorDelegate.getEditor().getXmlText(node);
+ if (t != null) {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(t);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns elements representing the given selection of canvas items.
+ *
+ * @param items Items to wrap in elements
+ * @return An array of wrapper elements. Never null.
+ */
+ @NonNull
+ static SimpleElement[] getAsElements(@NonNull List<SelectionItem> items) {
+ return getAsElements(items, null);
+ }
+
+ /**
+ * Returns elements representing the given selection of canvas items.
+ *
+ * @param items Items to wrap in elements
+ * @param primary The primary selected item which should be listed first
+ * @return An array of wrapper elements. Never null.
+ */
+ @NonNull
+ static SimpleElement[] getAsElements(
+ @NonNull List<SelectionItem> items,
+ @Nullable SelectionItem primary) {
+ List<SimpleElement> elements = new ArrayList<SimpleElement>();
+
+ if (primary != null) {
+ CanvasViewInfo vi = primary.getViewInfo();
+ SimpleElement e = vi.toSimpleElement();
+ e.setSelectionItem(primary);
+ elements.add(e);
+ }
+
+ for (SelectionItem cs : items) {
+ if (cs == primary) {
+ // Already handled
+ continue;
+ }
+
+ CanvasViewInfo vi = cs.getViewInfo();
+ SimpleElement e = vi.toSimpleElement();
+ e.setSelectionItem(cs);
+ elements.add(e);
+ }
+
+ return elements.toArray(new SimpleElement[elements.size()]);
+ }
+
+ /**
+ * Returns true if this selection item is a layout
+ *
+ * @return true if this selection item is a layout
+ */
+ public boolean isLayout() {
+ UiViewElementNode node = mCanvasViewInfo.getUiViewNode();
+ if (node != null) {
+ return node.getDescriptor().hasChildren();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the {@link SelectionHandles} for this {@link SelectionItem}. Never null.
+ *
+ * @return the {@link SelectionHandles} for this {@link SelectionItem}, never null
+ */
+ @NonNull
+ public SelectionHandles getSelectionHandles() {
+ if (mHandles == null) {
+ mHandles = new SelectionHandles(this);
+ }
+
+ return mHandles;
+ }
+
+ /**
+ * Returns the {@link ResizePolicy} for this item
+ *
+ * @return the {@link ResizePolicy} for this item, never null
+ */
+ @NonNull
+ public ResizePolicy getResizePolicy() {
+ if (mResizePolicy == null && mNodeProxy != null) {
+ mResizePolicy = ViewMetadataRepository.get().getResizePolicy(mNodeProxy.getFqcn());
+ }
+
+ return mResizePolicy;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
new file mode 100644
index 000000000..eb3d6f290
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
@@ -0,0 +1,1262 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * The {@link SelectionManager} manages the selection in the canvas editor.
+ * It holds (and can be asked about) the set of selected items, and it also has
+ * operations for manipulating the selection - such as toggling items, copying
+ * the selection to the clipboard, etc.
+ * <p/>
+ * This class implements {@link ISelectionProvider} so that it can delegate
+ * the selection provider from the {@link LayoutCanvasViewer}.
+ * <p/>
+ * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
+ * manager so that it can invoke its own fireSelectionChanged when the canvas'
+ * selection changes.
+ */
+public class SelectionManager implements ISelectionProvider {
+
+ private LayoutCanvas mCanvas;
+
+ /** The current selection list. The list is never null, however it can be empty. */
+ private final LinkedList<SelectionItem> mSelections = new LinkedList<SelectionItem>();
+
+ /** An unmodifiable view of {@link #mSelections}. */
+ private final List<SelectionItem> mUnmodifiableSelection =
+ Collections.unmodifiableList(mSelections);
+
+ /** Barrier set when updating the selection to prevent from recursively
+ * invoking ourselves. */
+ private boolean mInsideUpdateSelection;
+
+ /**
+ * The <em>current</em> alternate selection, if any, which changes when the Alt key is
+ * used during a selection. Can be null.
+ */
+ private CanvasAlternateSelection mAltSelection;
+
+ /** List of clients listening to selection changes. */
+ private final ListenerList mSelectionListeners = new ListenerList();
+
+ /**
+ * Constructs a new {@link SelectionManager} associated with the given layout canvas.
+ *
+ * @param layoutCanvas The layout canvas to create a {@link SelectionManager} for.
+ */
+ public SelectionManager(LayoutCanvas layoutCanvas) {
+ mCanvas = layoutCanvas;
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.add(listener);
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.remove(listener);
+ }
+
+ /**
+ * Returns the native {@link SelectionItem} list.
+ *
+ * @return An immutable list of {@link SelectionItem}. Can be empty but not null.
+ */
+ @NonNull
+ List<SelectionItem> getSelections() {
+ return mUnmodifiableSelection;
+ }
+
+ /**
+ * Return a snapshot/copy of the selection. Useful for clipboards etc where we
+ * don't want the returned copy to be affected by future edits to the selection.
+ *
+ * @return A copy of the current selection. Never null.
+ */
+ @NonNull
+ public List<SelectionItem> getSnapshot() {
+ if (mSelectionListeners.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return new ArrayList<SelectionItem>(mSelections);
+ }
+
+ /**
+ * Returns a {@link TreeSelection} where each {@link TreePath} item is
+ * actually a {@link CanvasViewInfo}.
+ */
+ @Override
+ public ISelection getSelection() {
+ if (mSelections.isEmpty()) {
+ return TreeSelection.EMPTY;
+ }
+
+ ArrayList<TreePath> paths = new ArrayList<TreePath>();
+
+ for (SelectionItem cs : mSelections) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null) {
+ paths.add(getTreePath(vi));
+ }
+ }
+
+ return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
+ }
+
+ /**
+ * Create a {@link TreePath} from the given view info
+ *
+ * @param viewInfo the view info to look up a tree path for
+ * @return a {@link TreePath} for the given view info
+ */
+ public static TreePath getTreePath(CanvasViewInfo viewInfo) {
+ ArrayList<Object> segments = new ArrayList<Object>();
+ while (viewInfo != null) {
+ segments.add(0, viewInfo);
+ viewInfo = viewInfo.getParent();
+ }
+
+ return new TreePath(segments.toArray());
+ }
+
+ /**
+ * Sets the selection. It must be an {@link ITreeSelection} where each segment
+ * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
+ * as an empty selection.
+ * <p/>
+ * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
+ * in response to an <em>outside</em> selection (compatible with ours) that has
+ * changed. Typically it means the outline selection has changed and we're
+ * synchronizing ours to match.
+ */
+ @Override
+ public void setSelection(ISelection selection) {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+
+ boolean changed = false;
+ try {
+ mInsideUpdateSelection = true;
+
+ if (selection == null) {
+ selection = TreeSelection.EMPTY;
+ }
+
+ if (selection instanceof ITreeSelection) {
+ ITreeSelection treeSel = (ITreeSelection) selection;
+
+ if (treeSel.isEmpty()) {
+ // Clear existing selection, if any
+ if (!mSelections.isEmpty()) {
+ mSelections.clear();
+ mAltSelection = null;
+ updateActionsFromSelection();
+ redraw();
+ }
+ return;
+ }
+
+ boolean redoLayout = false;
+
+ // Create a list of all currently selected view infos
+ Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
+ for (SelectionItem cs : mSelections) {
+ oldSelected.add(cs.getViewInfo());
+ }
+
+ // Go thru new selection and take care of selecting new items
+ // or marking those which are the same as in the current selection
+ for (TreePath path : treeSel.getPaths()) {
+ Object seg = path.getLastSegment();
+ if (seg instanceof CanvasViewInfo) {
+ CanvasViewInfo newVi = (CanvasViewInfo) seg;
+ if (oldSelected.contains(newVi)) {
+ // This view info is already selected. Remove it from the
+ // oldSelected list so that we don't deselect it later.
+ oldSelected.remove(newVi);
+ } else {
+ // This view info is not already selected. Select it now.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+ // otherwise add it.
+ mSelections.add(createSelection(newVi));
+ changed = true;
+ }
+ if (newVi.isInvisible()) {
+ redoLayout = true;
+ }
+ } else {
+ // Unrelated selection (e.g. user clicked in the Project Explorer
+ // or something) -- just ignore these
+ return;
+ }
+ }
+
+ // Deselect old selected items that are not in the new one
+ for (CanvasViewInfo vi : oldSelected) {
+ if (vi.isExploded()) {
+ redoLayout = true;
+ }
+ deselect(vi);
+ changed = true;
+ }
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+ }
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+
+ if (changed) {
+ redraw();
+ fireSelectionChanged();
+ updateActionsFromSelection();
+ }
+ }
+
+ /**
+ * The menu has been activated; ensure that the menu click is over the existing
+ * selection, and if not, update the selection.
+ *
+ * @param e the {@link MenuDetectEvent} which triggered the menu
+ */
+ public void menuClick(MenuDetectEvent e) {
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+ // Right click button is used to display a context menu.
+ // If there's an existing selection and the click is anywhere in this selection
+ // and there are no modifiers being used, we don't want to change the selection.
+ // Otherwise we select the item under the cursor.
+
+ for (SelectionItem cs : mSelections) {
+ if (cs.isRoot()) {
+ continue;
+ }
+ if (cs.getRect().contains(p.x, p.y)) {
+ // The cursor is inside the selection. Don't change anything.
+ return;
+ }
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ selectSingle(vi);
+ }
+
+ /**
+ * Performs selection for a mouse event.
+ * <p/>
+ * Shift key (or Command on the Mac) is used to toggle in multi-selection.
+ * Alt key is used to cycle selection through objects at the same level than
+ * the one pointed at (i.e. click on an object then alt-click to cycle).
+ *
+ * @param e The mouse event which triggered the selection. Cannot be null.
+ * The modifier key mask will be used to determine whether this
+ * is a plain select or a toggle, etc.
+ */
+ public void select(MouseEvent e) {
+ boolean isMultiClick = (e.stateMask & SWT.SHIFT) != 0 ||
+ // On Mac, the Command key is the normal toggle accelerator
+ ((SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) &&
+ (e.stateMask & SWT.COMMAND) != 0);
+ boolean isCycleClick = (e.stateMask & SWT.ALT) != 0;
+
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+ if (e.button == 3) {
+ // Right click button is used to display a context menu.
+ // If there's an existing selection and the click is anywhere in this selection
+ // and there are no modifiers being used, we don't want to change the selection.
+ // Otherwise we select the item under the cursor.
+
+ if (!isCycleClick && !isMultiClick) {
+ for (SelectionItem cs : mSelections) {
+ if (cs.getRect().contains(p.x, p.y)) {
+ // The cursor is inside the selection. Don't change anything.
+ return;
+ }
+ }
+ }
+
+ } else if (e.button != 1) {
+ // Click was done with something else than the left button for normal selection
+ // or the right button for context menu.
+ // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
+ // anything, so let's not change the selection.
+ return;
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+
+ if (vi != null && vi.isHidden()) {
+ vi = vi.getParent();
+ }
+
+ if (isMultiClick && !isCycleClick) {
+ // Case where shift is pressed: pointed object is toggled.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ // If nothing has been found at the cursor, assume it might be a user error
+ // and avoid clearing the existing selection.
+
+ if (vi != null) {
+ // toggle this selection on-off: remove it if already selected
+ if (deselect(vi)) {
+ if (vi.isExploded()) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+ return;
+ }
+
+ // otherwise add it.
+ mSelections.add(createSelection(vi));
+ fireSelectionChanged();
+ redraw();
+ }
+
+ } else if (isCycleClick) {
+ // Case where alt is pressed: select or cycle the object pointed at.
+
+ // Note: if shift and alt are pressed, shift is ignored. The alternate selection
+ // mechanism does not reset the current multiple selection unless they intersect.
+
+ // We need to remember the "origin" of the alternate selection, to be
+ // able to continue cycling through it later. If there's no alternate selection,
+ // create one. If there's one but not for the same origin object, create a new
+ // one too.
+ if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
+ mAltSelection = new CanvasAlternateSelection(
+ vi, mCanvas.getViewHierarchy().findAltViewInfoAt(p));
+
+ // deselect them all, in case they were partially selected
+ deselectAll(mAltSelection.getAltViews());
+
+ // select the current one
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ if (vi2 != null) {
+ mSelections.addFirst(createSelection(vi2));
+ fireSelectionChanged();
+ }
+ } else {
+ // We're trying to cycle through the current alternate selection.
+ // First remove the current object.
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ deselect(vi2);
+
+ // Now select the next one.
+ vi2 = mAltSelection.getNext();
+ if (vi2 != null) {
+ mSelections.addFirst(createSelection(vi2));
+ fireSelectionChanged();
+ }
+ }
+ redraw();
+
+ } else {
+ // Case where no modifier is pressed: either select or reset the selection.
+ selectSingle(vi);
+ }
+ }
+
+ /**
+ * Removes all the currently selected item and only select the given item.
+ * Issues a redraw() if the selection changes.
+ *
+ * @param vi The new selected item if non-null. Selection becomes empty if null.
+ * @return the item selected, or null if the selection was cleared (e.g. vi was null)
+ */
+ @Nullable
+ SelectionItem selectSingle(CanvasViewInfo vi) {
+ SelectionItem item = null;
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ if (vi == null) {
+ // The user clicked outside the bounds of the root element; in that case, just
+ // select the root element.
+ vi = mCanvas.getViewHierarchy().getRoot();
+ }
+
+ boolean redoLayout = hasExplodedItems();
+
+ // reset (multi)selection if any
+ if (!mSelections.isEmpty()) {
+ if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
+ // CanvasSelection remains the same, don't touch it.
+ return mSelections.getFirst();
+ }
+ mSelections.clear();
+ }
+
+ if (vi != null) {
+ item = createSelection(vi);
+ mSelections.add(item);
+ if (vi.isInvisible()) {
+ redoLayout = true;
+ }
+ }
+ fireSelectionChanged();
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+
+ return item;
+ }
+
+ /** Returns true if the view hierarchy is showing exploded items. */
+ private boolean hasExplodedItems() {
+ for (SelectionItem item : mSelections) {
+ if (item.getViewInfo().isExploded()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Selects the given set of {@link CanvasViewInfo}s. This is similar to
+ * {@link #selectSingle} but allows you to make a multi-selection. Issues a
+ * {@link #redraw()}.
+ *
+ * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
+ * selected, or null or empty to clear the selection.
+ */
+ /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ boolean redoLayout = hasExplodedItems();
+
+ mSelections.clear();
+ if (viewInfos != null) {
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ mSelections.add(createSelection(viewInfo));
+ if (viewInfo.isInvisible()) {
+ redoLayout = true;
+ }
+ }
+ }
+
+ fireSelectionChanged();
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+ }
+
+ public void select(Collection<INode> nodes) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>(nodes.size());
+ for (INode node : nodes) {
+ CanvasViewInfo info = mCanvas.getViewHierarchy().findViewInfoFor(node);
+ if (info != null) {
+ infos.add(info);
+ }
+ }
+ selectMultiple(infos);
+ }
+
+ /**
+ * Selects the visual element corresponding to the given XML node
+ * @param xmlNode The Node whose element we want to select.
+ */
+ /* package */ void select(Node xmlNode) {
+ if (xmlNode == null) {
+ return;
+ } else if (xmlNode.getNodeType() == Node.TEXT_NODE) {
+ xmlNode = xmlNode.getParentNode();
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoFor(xmlNode);
+ if (vi != null && !vi.isRoot()) {
+ selectSingle(vi);
+ }
+ }
+
+ /**
+ * Selects any views that overlap the given selection rectangle.
+ *
+ * @param topLeft The top left corner defining the selection rectangle.
+ * @param bottomRight The bottom right corner defining the selection
+ * rectangle.
+ * @param toggled A set of {@link CanvasViewInfo}s that should be toggled
+ * rather than just added.
+ */
+ public void selectWithin(LayoutPoint topLeft, LayoutPoint bottomRight,
+ Collection<CanvasViewInfo> toggled) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ Collection<CanvasViewInfo> viewInfos = viewHierarchy.findWithin(topLeft, bottomRight);
+
+ if (toggled.size() > 0) {
+ // Copy; we're not allowed to touch the passed in collection
+ Set<CanvasViewInfo> result = new HashSet<CanvasViewInfo>(toggled);
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ if (toggled.contains(viewInfo)) {
+ result.remove(viewInfo);
+ } else {
+ result.add(viewInfo);
+ }
+ }
+ viewInfos = result;
+ }
+
+ mSelections.clear();
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ if (viewInfo.isHidden()) {
+ continue;
+ }
+ mSelections.add(createSelection(viewInfo));
+ }
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /**
+ * Clears the selection and then selects everything (all views and all their
+ * children).
+ */
+ public void selectAll() {
+ // First clear the current selection, if any.
+ mSelections.clear();
+ mAltSelection = null;
+
+ // Now select everything if there's a valid layout
+ for (CanvasViewInfo vi : mCanvas.getViewHierarchy().findAllViewInfos(false)) {
+ mSelections.add(createSelection(vi));
+ }
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /** Clears the selection */
+ public void selectNone() {
+ mSelections.clear();
+ mAltSelection = null;
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /** Selects the parent of the current selection */
+ public void selectParent() {
+ if (mSelections.size() == 1) {
+ CanvasViewInfo parent = mSelections.get(0).getViewInfo().getParent();
+ if (parent != null) {
+ selectSingle(parent);
+ }
+ }
+ }
+
+ /** Finds all widgets in the layout that have the same type as the primary */
+ public void selectSameType() {
+ // Find all
+ if (mSelections.size() == 1) {
+ CanvasViewInfo viewInfo = mSelections.get(0).getViewInfo();
+ ElementDescriptor descriptor = viewInfo.getUiViewNode().getDescriptor();
+ mSelections.clear();
+ mAltSelection = null;
+ addSameType(mCanvas.getViewHierarchy().getRoot(), descriptor);
+ fireSelectionChanged();
+ redraw();
+ }
+ }
+
+ /** Helper for {@link #selectSameType} */
+ private void addSameType(CanvasViewInfo root, ElementDescriptor descriptor) {
+ if (root.getUiViewNode().getDescriptor() == descriptor) {
+ mSelections.add(createSelection(root));
+ }
+
+ for (CanvasViewInfo child : root.getChildren()) {
+ addSameType(child, descriptor);
+ }
+ }
+
+ /** Selects the siblings of the primary */
+ public void selectSiblings() {
+ // Find all
+ if (mSelections.size() == 1) {
+ CanvasViewInfo vi = mSelections.get(0).getViewInfo();
+ mSelections.clear();
+ mAltSelection = null;
+ CanvasViewInfo parent = vi.getParent();
+ if (parent == null) {
+ selectNone();
+ } else {
+ for (CanvasViewInfo child : parent.getChildren()) {
+ mSelections.add(createSelection(child));
+ }
+ fireSelectionChanged();
+ redraw();
+ }
+ }
+ }
+
+ /**
+ * Returns true if and only if there is currently more than one selected
+ * item.
+ *
+ * @return True if more than one item is selected
+ */
+ public boolean hasMultiSelection() {
+ return mSelections.size() > 1;
+ }
+
+ /**
+ * Deselects a view info. Returns true if the object was actually selected.
+ * Callers are responsible for calling redraw() and updateOulineSelection()
+ * after.
+ * @param canvasViewInfo The item to deselect.
+ * @return True if the object was successfully removed from the selection.
+ */
+ public boolean deselect(CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return false;
+ }
+
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+ if (canvasViewInfo == s.getViewInfo()) {
+ it.remove();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Deselects multiple view infos.
+ * Callers are responsible for calling redraw() and updateOulineSelection() after.
+ */
+ private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+ if (canvasViewInfos.contains(s.getViewInfo())) {
+ it.remove();
+ }
+ }
+ }
+
+ /** Sync the selection with an updated view info tree */
+ void sync() {
+ // Check if the selection is still the same (based on the object keys)
+ // and eventually recompute their bounds.
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+
+ // Check if the selected object still exists
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ UiViewElementNode key = s.getViewInfo().getUiViewNode();
+ CanvasViewInfo vi = viewHierarchy.findViewInfoFor(key);
+
+ // Remove the previous selection -- if the selected object still exists
+ // we need to recompute its bounds in case it moved so we'll insert a new one
+ // at the same place.
+ it.remove();
+ if (vi == null) {
+ vi = findCorresponding(s.getViewInfo(), viewHierarchy.getRoot());
+ }
+ if (vi != null) {
+ it.add(createSelection(vi));
+ }
+ }
+ fireSelectionChanged();
+
+ // remove the current alternate selection views
+ mAltSelection = null;
+ }
+
+ /** Finds the corresponding {@link CanvasViewInfo} in the new hierarchy */
+ private CanvasViewInfo findCorresponding(CanvasViewInfo old, CanvasViewInfo newRoot) {
+ CanvasViewInfo oldParent = old.getParent();
+ if (oldParent != null) {
+ CanvasViewInfo newParent = findCorresponding(oldParent, newRoot);
+ if (newParent == null) {
+ return null;
+ }
+
+ List<CanvasViewInfo> oldSiblings = oldParent.getChildren();
+ List<CanvasViewInfo> newSiblings = newParent.getChildren();
+ Iterator<CanvasViewInfo> oldIterator = oldSiblings.iterator();
+ Iterator<CanvasViewInfo> newIterator = newSiblings.iterator();
+ while (oldIterator.hasNext() && newIterator.hasNext()) {
+ CanvasViewInfo oldSibling = oldIterator.next();
+ CanvasViewInfo newSibling = newIterator.next();
+
+ if (oldSibling.getName().equals(newSibling.getName())) {
+ // Structure has changed: can't do a proper search
+ return null;
+ }
+
+ if (oldSibling == old) {
+ return newSibling;
+ }
+ }
+ } else {
+ return newRoot;
+ }
+
+ return null;
+ }
+
+ /**
+ * Notifies listeners that the selection has changed.
+ */
+ private void fireSelectionChanged() {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+ try {
+ mInsideUpdateSelection = true;
+
+ final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
+
+ SafeRunnable.run(new SafeRunnable() {
+ @Override
+ public void run() {
+ for (Object listener : mSelectionListeners.getListeners()) {
+ ((ISelectionChangedListener) listener).selectionChanged(event);
+ }
+ }
+ });
+
+ updateActionsFromSelection();
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+ }
+
+ /**
+ * Updates menu actions and the layout action bar after a selection change - these are
+ * actions that depend on the selection
+ */
+ private void updateActionsFromSelection() {
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ if (editor != null) {
+ // Update menu actions that depend on the selection
+ mCanvas.updateMenuActionState();
+
+ // Update the layout actions bar
+ LayoutActionBar layoutActionBar = editor.getGraphicalEditor().getLayoutActionBar();
+ layoutActionBar.updateSelection();
+ }
+ }
+
+ /**
+ * Sanitizes the selection for a copy/cut or drag operation.
+ * <p/>
+ * Sanitizes the list to make sure all elements have a valid XML attached to it,
+ * that is remove element that have no XML to avoid having to make repeated such
+ * checks in various places after.
+ * <p/>
+ * In case of multiple selection, we also need to remove all children when their
+ * parent is already selected since parents will always be added with all their
+ * children.
+ * <p/>
+ *
+ * @param selection The selection list to be sanitized <b>in-place</b>.
+ * The <code>selection</code> argument should not be {@link #mSelections} -- the
+ * given list is going to be altered and we should never alter the user-made selection.
+ * Instead the caller should provide its own copy.
+ */
+ /* package */ static void sanitize(List<SelectionItem> selection) {
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ for (Iterator<SelectionItem> it = selection.iterator(); it.hasNext(); ) {
+ SelectionItem cs = it.next();
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi == null ? null : vi.getUiViewNode();
+ Node node = key == null ? null : key.getXmlNode();
+ if (node == null) {
+ // Missing ViewInfo or view key or XML, discard this.
+ it.remove();
+ continue;
+ }
+
+ if (vi != null) {
+ for (Iterator<SelectionItem> it2 = selection.iterator();
+ it2.hasNext(); ) {
+ SelectionItem cs2 = it2.next();
+ if (cs != cs2) {
+ CanvasViewInfo vi2 = cs2.getViewInfo();
+ if (vi.isParent(vi2)) {
+ // vi2 is a parent for vi. Remove vi.
+ it.remove();
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Selects the given list of nodes in the canvas, and returns true iff the
+ * attempt to select was successful.
+ *
+ * @param nodes The collection of nodes to be selected
+ * @param indices A list of indices within the parent for each node, or null
+ * @return True if and only if all nodes were successfully selected
+ */
+ public boolean selectDropped(List<INode> nodes, List<Integer> indices) {
+ assert indices == null || nodes.size() == indices.size();
+
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+
+ // Look up a list of view infos which correspond to the nodes.
+ final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>();
+ for (int i = 0, n = nodes.size(); i < n; i++) {
+ INode node = nodes.get(i);
+
+ CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor(node);
+
+ // There are two scenarios where looking up a view info fails.
+ // The first one is that the node was just added and the render has not yet
+ // happened, so the ViewHierarchy has no record of the node. In this case
+ // there is nothing we can do, and the method will return false (which the
+ // caller will use to schedule a second attempt later).
+ // The second scenario is where the nodes *change identity*. This isn't
+ // common, but when a drop handler makes a lot of changes to its children,
+ // for example when dropping into a GridLayout where attributes are adjusted
+ // on nearly all the other children to update row or column attributes
+ // etc, then in some cases Eclipse's DOM model changes the identities of
+ // the nodes when applying all the edits, so the new Node we created (as
+ // well as possibly other nodes) are no longer the children we observe
+ // after the edit, and there are new copies there instead. In this case
+ // the UiViewModel also fails to map the nodes. To work around this,
+ // we track the *indices* (within the parent) during a drop, such that we
+ // know which children (according to their positions) the given nodes
+ // are supposed to map to, and then we use these view infos instead.
+ if (viewInfo == null && node instanceof NodeProxy && indices != null) {
+ INode parent = node.getParent();
+ CanvasViewInfo parentViewInfo = viewHierarchy.findViewInfoFor(parent);
+ if (parentViewInfo != null) {
+ UiViewElementNode parentUiNode = parentViewInfo.getUiViewNode();
+ if (parentUiNode != null) {
+ List<UiElementNode> children = parentUiNode.getUiChildren();
+ int index = indices.get(i);
+ if (index >= 0 && index < children.size()) {
+ UiElementNode replacedNode = children.get(index);
+ viewInfo = viewHierarchy.findViewInfoFor(replacedNode);
+ }
+ }
+ }
+ }
+
+ if (viewInfo != null) {
+ if (nodes.size() > 1 && viewInfo.isHidden()) {
+ // Skip spacers - unless you're dropping just one
+ continue;
+ }
+ if (GridLayoutRule.sDebugGridLayout && (viewInfo.getName().equals(FQCN_SPACE)
+ || viewInfo.getName().equals(FQCN_SPACE_V7))) {
+ // In debug mode they might not be marked as hidden but we never never
+ // want to select these guys
+ continue;
+ }
+ newChildren.add(viewInfo);
+ }
+ }
+ boolean found = nodes.size() == newChildren.size();
+
+ if (found || newChildren.size() > 0) {
+ mCanvas.getSelectionManager().selectMultiple(newChildren);
+ }
+
+ return found;
+ }
+
+ /**
+ * Update the outline selection to select the given nodes, asynchronously.
+ * @param nodes The nodes to be selected
+ */
+ public void setOutlineSelection(final List<INode> nodes) {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ selectDropped(nodes, null /* indices */);
+ syncOutlineSelection();
+ }
+ });
+ }
+
+ /**
+ * Syncs the current selection to the outline, synchronously.
+ */
+ public void syncOutlineSelection() {
+ OutlinePage outlinePage = mCanvas.getOutlinePage();
+ IWorkbenchPartSite site = outlinePage.getEditor().getSite();
+ ISelectionProvider selectionProvider = site.getSelectionProvider();
+ ISelection selection = selectionProvider.getSelection();
+ if (selection != null) {
+ outlinePage.setSelection(selection);
+ }
+ }
+
+ private void redraw() {
+ mCanvas.redraw();
+ }
+
+ SelectionItem createSelection(CanvasViewInfo vi) {
+ return new SelectionItem(mCanvas, vi);
+ }
+
+ /**
+ * Returns true if there is nothing selected
+ *
+ * @return true if there is nothing selected
+ */
+ public boolean isEmpty() {
+ return mSelections.size() == 0;
+ }
+
+ /**
+ * "Select" context menu which lists various menu options related to selection:
+ * <ul>
+ * <li> Select All
+ * <li> Select Parent
+ * <li> Select None
+ * <li> Select Siblings
+ * <li> Select Same Type
+ * </ul>
+ * etc.
+ */
+ public static class SelectionMenu extends SubmenuAction {
+ private final GraphicalEditorPart mEditor;
+
+ public SelectionMenu(GraphicalEditorPart editor) {
+ super("Select");
+ mEditor = editor;
+ }
+
+ @Override
+ public String getId() {
+ return "-selectionmenu"; //$NON-NLS-1$
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ boolean selectedOne = selections.size() == 1;
+ boolean notRoot = selectedOne && !selections.get(0).isRoot();
+ boolean haveSelection = selections.size() > 0;
+
+ Action a;
+ a = selectionManager.new SelectAction("Select Parent\tEsc", SELECT_PARENT);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(notRoot);
+ a.setAccelerator(SWT.ESC);
+
+ a = selectionManager.new SelectAction("Select Siblings", SELECT_SIBLINGS);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(notRoot);
+
+ a = selectionManager.new SelectAction("Select Same Type", SELECT_SAME_TYPE);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(selectedOne);
+
+ new Separator().fill(menu, -1);
+
+ // Special case for Select All: Use global action
+ a = canvas.getSelectAllAction();
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(true);
+
+ a = selectionManager.new SelectAction("Deselect All", SELECT_NONE);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(haveSelection);
+ }
+ }
+
+ private static final int SELECT_PARENT = 1;
+ private static final int SELECT_SIBLINGS = 2;
+ private static final int SELECT_SAME_TYPE = 3;
+ private static final int SELECT_NONE = 4; // SELECT_ALL is handled separately
+
+ private class SelectAction extends Action {
+ private final int mType;
+
+ public SelectAction(String title, int type) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mType = type;
+ }
+
+ @Override
+ public void run() {
+ switch (mType) {
+ case SELECT_NONE:
+ selectNone();
+ break;
+ case SELECT_PARENT:
+ selectParent();
+ break;
+ case SELECT_SAME_TYPE:
+ selectSameType();
+ break;
+ case SELECT_SIBLINGS:
+ selectSiblings();
+ break;
+ }
+
+ List<INode> nodes = new ArrayList<INode>();
+ for (SelectionItem item : getSelections()) {
+ nodes.add(item.getNode());
+ }
+ setOutlineSelection(nodes);
+ }
+ }
+
+ public Pair<SelectionItem, SelectionHandle> findHandle(ControlPoint controlPoint) {
+ if (!isEmpty()) {
+ LayoutPoint layoutPoint = controlPoint.toLayout();
+ int distance = (int) ((PIXEL_MARGIN + PIXEL_RADIUS) / mCanvas.getScale());
+
+ for (SelectionItem item : getSelections()) {
+ SelectionHandles handles = item.getSelectionHandles();
+ // See if it's over the selection handles
+ SelectionHandle handle = handles.findHandle(layoutPoint, distance);
+ if (handle != null) {
+ return Pair.of(item, handle);
+ }
+ }
+
+ }
+ return null;
+ }
+
+ /** Performs the default action provided by the currently selected view */
+ public void performDefaultAction() {
+ final List<SelectionItem> selections = getSelections();
+ if (selections.size() > 0) {
+ NodeProxy primary = selections.get(0).getNode();
+ if (primary != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ final String id = rulesEngine.callGetDefaultActionId(primary);
+ if (id == null) {
+ return;
+ }
+ final List<RuleAction> actions = rulesEngine.callGetContextMenu(primary);
+ if (actions == null) {
+ return;
+ }
+ RuleAction matching = null;
+ for (RuleAction a : actions) {
+ if (id.equals(a.getId())) {
+ matching = a;
+ break;
+ }
+ }
+ if (matching == null) {
+ return;
+ }
+ final List<INode> selectedNodes = new ArrayList<INode>();
+ for (SelectionItem item : selections) {
+ NodeProxy n = item.getNode();
+ if (n != null) {
+ selectedNodes.add(n);
+ }
+ }
+ final RuleAction action = matching;
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(action.getTitle(),
+ new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, selectedNodes,
+ action.getId(), null);
+ LayoutCanvas canvas = mCanvas;
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /** Performs renaming the selected views */
+ public void performRename() {
+ final List<SelectionItem> selections = getSelections();
+ if (selections.size() > 0) {
+ NodeProxy primary = selections.get(0).getNode();
+ if (primary != null) {
+ performRename(primary, selections);
+ }
+ }
+ }
+
+ /**
+ * Performs renaming the given node.
+ *
+ * @param primary the node to be renamed, or the primary node (to get the
+ * current value from if more than one node should be renamed)
+ * @param selections if not null, a list of nodes to apply the setting to
+ * (which should include the primary)
+ * @return the result of the renaming operation
+ */
+ @NonNull
+ public RenameResult performRename(
+ final @NonNull INode primary,
+ final @Nullable List<SelectionItem> selections) {
+ String id = primary.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ RenameResult result = RenameResourceWizard.renameResource(
+ mCanvas.getShell(),
+ mCanvas.getEditorDelegate().getGraphicalEditor().getProject(),
+ ResourceType.ID, BaseViewRule.stripIdPrefix(id), null, true /*canClear*/);
+ if (result.isCanceled()) {
+ return result;
+ } else if (!result.isUnavailable()) {
+ return result;
+ }
+ }
+ String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID);
+ currentId = BaseViewRule.stripIdPrefix(currentId);
+ InputDialog d = new InputDialog(
+ AdtPlugin.getDisplay().getActiveShell(),
+ "Set ID",
+ "New ID:",
+ currentId,
+ ResourceNameValidator.create(false, (IProject) null, ResourceType.ID));
+ if (d.open() == Window.OK) {
+ final String s = d.getValue();
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID",
+ new Runnable() {
+ @Override
+ public void run() {
+ String newId = s;
+ newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s);
+ if (selections != null) {
+ for (SelectionItem item : selections) {
+ NodeProxy node = item.getNode();
+ if (node != null) {
+ node.setAttribute(ANDROID_URI, ATTR_ID, newId);
+ }
+ }
+ } else {
+ primary.setAttribute(ANDROID_URI, ATTR_ID, newId);
+ }
+
+ LayoutCanvas canvas = mCanvas;
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+ });
+ return RenameResult.name(BaseViewRule.stripIdPrefix(s));
+ } else {
+ return RenameResult.canceled();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java
new file mode 100644
index 000000000..97d048108
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+
+import org.eclipse.swt.graphics.GC;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link SelectionOverlay} paints the current selection as an overlay.
+ */
+public class SelectionOverlay extends Overlay {
+ private final LayoutCanvas mCanvas;
+ private boolean mHidden;
+
+ /**
+ * Constructs a new {@link SelectionOverlay} tied to the given canvas.
+ *
+ * @param canvas the associated canvas
+ */
+ public SelectionOverlay(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * Set whether the selection overlay should be hidden. This is done during some
+ * gestures like resize where the new bounds could be confused with the current
+ * selection bounds.
+ *
+ * @param hidden when true, hide the selection bounds, when false, unhide.
+ */
+ public void setHidden(boolean hidden) {
+ mHidden = hidden;
+ }
+
+ /**
+ * Paints the selection.
+ *
+ * @param selectionManager The {@link SelectionManager} holding the
+ * selection.
+ * @param gcWrapper The graphics context wrapper for the layout rules to use.
+ * @param gc The SWT graphics object
+ * @param rulesEngine The {@link RulesEngine} holding the rules.
+ */
+ public void paint(SelectionManager selectionManager, GCWrapper gcWrapper,
+ GC gc, RulesEngine rulesEngine) {
+ if (mHidden) {
+ return;
+ }
+
+ List<SelectionItem> selections = selectionManager.getSelections();
+ int n = selections.size();
+ if (n > 0) {
+ List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>();
+ boolean isMultipleSelection = n > 1;
+ for (SelectionItem s : selections) {
+ if (s.isRoot()) {
+ // The root selection is never painted
+ continue;
+ }
+
+ NodeProxy node = s.getNode();
+ if (node != null) {
+ paintSelection(gcWrapper, gc, s, isMultipleSelection);
+ selectedNodes.add(node);
+ }
+ }
+
+ if (selectedNodes.size() > 0) {
+ paintSelectionFeedback(gcWrapper, selectedNodes, rulesEngine);
+ } else {
+ CanvasViewInfo root = mCanvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ NodeProxy parent = mCanvas.getNodeFactory().create(root);
+ rulesEngine.callPaintSelectionFeedback(gcWrapper,
+ parent, Collections.<INode>emptyList(), root.getViewObject());
+ }
+ }
+
+ if (n == 1) {
+ NodeProxy node = selections.get(0).getNode();
+ if (node != null) {
+ paintHints(gcWrapper, node, rulesEngine);
+ }
+ }
+ } else {
+ CanvasViewInfo root = mCanvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ NodeProxy parent = mCanvas.getNodeFactory().create(root);
+ rulesEngine.callPaintSelectionFeedback(gcWrapper,
+ parent, Collections.<INode>emptyList(), root.getViewObject());
+ }
+ }
+ }
+
+ /** Paint hint for current selection */
+ private void paintHints(GCWrapper gcWrapper, NodeProxy node, RulesEngine rulesEngine) {
+ INode parent = node.getParent();
+ if (parent instanceof NodeProxy) {
+ NodeProxy parentNode = (NodeProxy) parent;
+ List<String> infos = rulesEngine.callGetSelectionHint(parentNode, node);
+ if (infos != null && infos.size() > 0) {
+ gcWrapper.useStyle(DrawingStyle.HELP);
+
+ Rect b = mCanvas.getImageOverlay().getImageBounds();
+ if (b == null) {
+ return;
+ }
+
+ // Compute the location to display the help. This is done in
+ // layout coordinates, so we need to apply the scale in reverse
+ // when making pixel margins
+ // TODO: We could take the Canvas dimensions into account to see
+ // where there is more room.
+ // TODO: The scrollbars should take the presence of hint text
+ // into account.
+ double scale = mCanvas.getScale();
+ int x, y;
+ if (b.w > b.h) {
+ x = (int) (b.x + 3 / scale);
+ y = (int) (b.y + b.h + 6 / scale);
+ } else {
+ x = (int) (b.x + b.w + 6 / scale);
+ y = (int) (b.y + 3 / scale);
+ }
+ gcWrapper.drawBoxedStrings(x, y, infos);
+ }
+ }
+ }
+
+ private void paintSelectionFeedback(GCWrapper gcWrapper, List<NodeProxy> nodes,
+ RulesEngine rulesEngine) {
+ // Add fastpath for n=1
+
+ // Group nodes into parent/child groups
+ Set<INode> parents = new HashSet<INode>();
+ for (INode node : nodes) {
+ INode parent = node.getParent();
+ if (/*parent == null || */parent instanceof NodeProxy) {
+ NodeProxy parentNode = (NodeProxy) parent;
+ parents.add(parentNode);
+ }
+ }
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ for (INode parent : parents) {
+ List<INode> children = new ArrayList<INode>();
+ for (INode node : nodes) {
+ INode nodeParent = node.getParent();
+ if (nodeParent == parent) {
+ children.add(node);
+ }
+ }
+ CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor((NodeProxy) parent);
+ Object view = viewInfo != null ? viewInfo.getViewObject() : null;
+
+ rulesEngine.callPaintSelectionFeedback(gcWrapper,
+ (NodeProxy) parent, children, view);
+ }
+ }
+
+ /** Called by the canvas when a view is being selected. */
+ private void paintSelection(IGraphics gc, GC swtGc, SelectionItem item,
+ boolean isMultipleSelection) {
+ CanvasViewInfo view = item.getViewInfo();
+ if (view.isHidden()) {
+ return;
+ }
+
+ NodeProxy selectedNode = item.getNode();
+ Rect r = selectedNode.getBounds();
+ if (!r.isValid()) {
+ return;
+ }
+
+ gc.useStyle(DrawingStyle.SELECTION);
+
+ Margins insets = mCanvas.getInsets(selectedNode.getFqcn());
+ int x1 = r.x;
+ int y1 = r.y;
+ int x2 = r.x2() + 1;
+ int y2 = r.y2() + 1;
+
+ if (insets != null) {
+ x1 += insets.left;
+ x2 -= insets.right;
+ y1 += insets.top;
+ y2 -= insets.bottom;
+ }
+
+ gc.drawRect(x1, y1, x2, y2);
+
+ // Paint sibling rectangles, if applicable
+ List<CanvasViewInfo> siblings = view.getNodeSiblings();
+ if (siblings != null) {
+ for (CanvasViewInfo sibling : siblings) {
+ if (sibling != view) {
+ r = SwtUtils.toRect(sibling.getSelectionRect());
+ gc.fillRect(r);
+ gc.drawRect(r);
+ }
+ }
+ }
+
+ // Paint selection handles. These are painted in control coordinates on the
+ // real SWT GC object rather than in layout coordinates on the GCWrapper,
+ // since we want them to have a fixed size that is independent of the
+ // screen zoom.
+ CanvasTransform horizontalTransform = mCanvas.getHorizontalTransform();
+ CanvasTransform verticalTransform = mCanvas.getVerticalTransform();
+ int radius = SelectionHandle.PIXEL_RADIUS;
+ int doubleRadius = 2 * radius;
+ for (SelectionHandle handle : item.getSelectionHandles()) {
+ int cx = horizontalTransform.translate(handle.centerX);
+ int cy = verticalTransform.translate(handle.centerY);
+
+ SwtDrawingStyle style = SwtDrawingStyle.of(DrawingStyle.SELECTION);
+ gc.setAlpha(style.getStrokeAlpha());
+ swtGc.fillRectangle(cx - radius, cy - radius, doubleRadius, doubleRadius);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java
new file mode 100644
index 000000000..d1d529e5a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java
@@ -0,0 +1,82 @@
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.widgets.Menu;
+
+import java.util.List;
+
+/**
+ * Action which creates a submenu for the "Show Included In" action
+ */
+class ShowWithinMenu extends SubmenuAction {
+ private LayoutEditorDelegate mEditorDelegate;
+
+ ShowWithinMenu(LayoutEditorDelegate editorDelegate) {
+ super("Show Included In");
+ mEditorDelegate = editorDelegate;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor();
+ IFile file = graphicalEditor.getEditedFile();
+ if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ IProject project = file.getProject();
+ IncludeFinder finder = IncludeFinder.get(project);
+ final List<Reference> includedBy = finder.getIncludedBy(file);
+
+ if (includedBy != null && includedBy.size() > 0) {
+ for (final Reference reference : includedBy) {
+ String title = reference.getDisplayName();
+ IAction action = new ShowWithinAction(title, reference);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ new Separator().fill(menu, -1);
+ }
+ IAction action = new ShowWithinAction("Nothing", null);
+ if (includedBy == null || includedBy.size() == 0) {
+ action.setEnabled(false);
+ }
+ new ActionContributionItem(action).fill(menu, -1);
+ } else {
+ addDisabledMessageItem("Not supported on platform");
+ }
+ }
+
+ /** Action to select one particular include-context */
+ private class ShowWithinAction extends Action {
+ private Reference mReference;
+
+ public ShowWithinAction(String title, Reference reference) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mReference = reference;
+ }
+
+ @Override
+ public boolean isChecked() {
+ Reference within = mEditorDelegate.getGraphicalEditor().getIncludedWithin();
+ if (within == null) {
+ return mReference == null;
+ } else {
+ return within.equals(mReference);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!isChecked()) {
+ mEditorDelegate.getGraphicalEditor().showIn(mReference);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
new file mode 100644
index 000000000..198c16484
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode.IAttribute;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents one XML attribute in a {@link SimpleElement}.
+ * <p/>
+ * The attribute is always represented by a namespace URI, a name and a value.
+ * The name cannot be empty.
+ * The namespace URI can be empty for an attribute without a namespace but is never null.
+ * The value can be empty but cannot be null.
+ * <p/>
+ * For a more detailed explanation of the purpose of this class,
+ * please see {@link SimpleXmlTransfer}.
+ */
+public class SimpleAttribute implements IAttribute {
+ private final String mName;
+ private final String mValue;
+ private final String mUri;
+
+ /**
+ * Creates a new {@link SimpleAttribute}.
+ * <p/>
+ * Any null value will be converted to an empty non-null string.
+ * However it is a semantic error to use an empty name -- no assertion is done though.
+ *
+ * @param uri The URI of the attribute.
+ * @param name The XML local name of the attribute.
+ * @param value The value of the attribute.
+ */
+ public SimpleAttribute(String uri, String name, String value) {
+ mUri = uri == null ? "" : uri;
+ mName = name == null ? "" : name;
+ mValue = value == null ? "" : value;
+ }
+
+ /**
+ * Returns the namespace URI of the attribute.
+ * Can be empty for an attribute without a namespace but is never null.
+ */
+ @Override
+ public @NonNull String getUri() {
+ return mUri;
+ }
+
+ /** Returns the XML local name of the attribute. Cannot be null nor empty. */
+ @Override
+ public @NonNull String getName() {
+ return mName;
+ }
+
+ /** Returns the value of the attribute. Cannot be null. Can be empty. */
+ @Override
+ public @NonNull String getValue() {
+ return mValue;
+ }
+
+ // reader and writer methods
+
+ @Override
+ public String toString() {
+ return String.format("@%s:%s=%s\n", //$NON-NLS-1$
+ mName,
+ mUri,
+ mValue);
+ }
+
+ private static final Pattern REGEXP =
+ Pattern.compile("[^@]*@([^:]+):([^=]*)=([^\n]*)\n*"); //$NON-NLS-1$
+
+ static SimpleAttribute parseString(String value) {
+ Matcher m = REGEXP.matcher(value);
+ if (m.matches()) {
+ return new SimpleAttribute(m.group(2), m.group(1), m.group(3));
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof SimpleAttribute) {
+ SimpleAttribute sa = (SimpleAttribute) obj;
+
+ return mName.equals(sa.mName) &&
+ mUri.equals(sa.mUri) &&
+ mValue.equals(sa.mValue);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ long c = mName.hashCode();
+ // uses the formula defined in java.util.List.hashCode()
+ c = 31*c + mUri.hashCode();
+ c = 31*c + mValue.hashCode();
+ if (c > 0x0FFFFFFFFL) {
+ // wrap any overflow
+ c = c ^ (c >> 32);
+ }
+ return (int)(c & 0x0FFFFFFFFL);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java
new file mode 100644
index 000000000..9acc8c25e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an XML element with a name, attributes and inner elements.
+ * <p/>
+ * The semantic of the element name is to be a fully qualified class name of a View to inflate.
+ * The element name is not expected to have a name space.
+ * <p/>
+ * For a more detailed explanation of the purpose of this class,
+ * please see {@link SimpleXmlTransfer}.
+ */
+public class SimpleElement implements IDragElement {
+
+ /** Version number of the internal serialized string format. */
+ private static final String FORMAT_VERSION = "3";
+
+ private final String mFqcn;
+ private final String mParentFqcn;
+ private final Rect mBounds;
+ private final Rect mParentBounds;
+ private final List<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>();
+ private final List<IDragElement> mElements = new ArrayList<IDragElement>();
+
+ private IDragAttribute[] mCachedAttributes = null;
+ private IDragElement[] mCachedElements = null;
+ private SelectionItem mSelectionItem;
+
+ /**
+ * Creates a new {@link SimpleElement} with the specified element name.
+ *
+ * @param fqcn A fully qualified class name of a View to inflate, e.g.
+ * "android.view.Button". Must not be null nor empty.
+ * @param parentFqcn The fully qualified class name of the parent of this element.
+ * Can be null but not empty.
+ * @param bounds The canvas bounds of the originating canvas node of the element.
+ * If null, a non-null invalid rectangle will be assigned.
+ * @param parentBounds The canvas bounds of the parent of this element. Can be null.
+ */
+ public SimpleElement(String fqcn, String parentFqcn, Rect bounds, Rect parentBounds) {
+ mFqcn = fqcn;
+ mParentFqcn = parentFqcn;
+ mBounds = bounds == null ? new Rect() : bounds.copy();
+ mParentBounds = parentBounds == null ? new Rect() : parentBounds.copy();
+ }
+
+ /**
+ * Returns the element name, which must match a fully qualified class name of
+ * a View to inflate.
+ */
+ @Override
+ public @NonNull String getFqcn() {
+ return mFqcn;
+ }
+
+ /**
+ * Returns the bounds of the element's node, if it originated from an existing
+ * canvas. The rectangle is invalid and non-null when the element originated
+ * from the object palette (unless it successfully rendered a preview)
+ */
+ @Override
+ public @NonNull Rect getBounds() {
+ return mBounds;
+ }
+
+ /**
+ * Returns the fully qualified class name of the parent, if the element originated
+ * from an existing canvas. Returns null if the element has no parent, such as a top
+ * level element or an element originating from the object palette.
+ */
+ @Override
+ public String getParentFqcn() {
+ return mParentFqcn;
+ }
+
+ /**
+ * Returns the bounds of the element's parent, absolute for the canvas, or null if there
+ * is no suitable parent. This is null when {@link #getParentFqcn()} is null.
+ */
+ @Override
+ public @NonNull Rect getParentBounds() {
+ return mParentBounds;
+ }
+
+ @Override
+ public @NonNull IDragAttribute[] getAttributes() {
+ if (mCachedAttributes == null) {
+ mCachedAttributes = mAttributes.toArray(new IDragAttribute[mAttributes.size()]);
+ }
+ return mCachedAttributes;
+ }
+
+ @Override
+ public IDragAttribute getAttribute(@Nullable String uri, @NonNull String localName) {
+ for (IDragAttribute attr : mAttributes) {
+ if (attr.getUri().equals(uri) && attr.getName().equals(localName)) {
+ return attr;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull IDragElement[] getInnerElements() {
+ if (mCachedElements == null) {
+ mCachedElements = mElements.toArray(new IDragElement[mElements.size()]);
+ }
+ return mCachedElements;
+ }
+
+ public void addAttribute(SimpleAttribute attr) {
+ mCachedAttributes = null;
+ mAttributes.add(attr);
+ }
+
+ public void addInnerElement(SimpleElement e) {
+ mCachedElements = null;
+ mElements.add(e);
+ }
+
+ @Override
+ public boolean isSame(@NonNull INode node) {
+ if (mSelectionItem != null) {
+ return node == mSelectionItem.getNode();
+ } else {
+ return node.getBounds().equals(mBounds);
+ }
+ }
+
+ void setSelectionItem(@Nullable SelectionItem selectionItem) {
+ mSelectionItem = selectionItem;
+ }
+
+ @Nullable
+ SelectionItem getSelectionItem() {
+ return mSelectionItem;
+ }
+
+ @Nullable
+ static SimpleElement findPrimary(SimpleElement[] elements, SelectionItem primary) {
+ if (elements == null || elements.length == 0) {
+ return null;
+ }
+
+ if (elements.length == 1 || primary == null) {
+ return elements[0];
+ }
+
+ for (SimpleElement element : elements) {
+ if (element.getSelectionItem() == primary) {
+ return element;
+ }
+ }
+
+ return elements[0];
+ }
+
+ // reader and writer methods
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{V=").append(FORMAT_VERSION);
+ sb.append(",N=").append(mFqcn);
+ if (mParentFqcn != null) {
+ sb.append(",P=").append(mParentFqcn);
+ }
+ if (mBounds != null && mBounds.isValid()) {
+ sb.append(String.format(",R=%d %d %d %d", mBounds.x, mBounds.y, mBounds.w, mBounds.h));
+ }
+ if (mParentBounds != null && mParentBounds.isValid()) {
+ sb.append(String.format(",Q=%d %d %d %d",
+ mParentBounds.x, mParentBounds.y, mParentBounds.w, mParentBounds.h));
+ }
+ sb.append('\n');
+ for (IDragAttribute a : mAttributes) {
+ sb.append(a.toString());
+ }
+ for (IDragElement e : mElements) {
+ sb.append(e.toString());
+ }
+ sb.append("}\n"); //$NON-NLS-1$
+ return sb.toString();
+ }
+
+ /** Parses a string containing one or more elements. */
+ static SimpleElement[] parseString(String value) {
+ ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
+ String[] lines = value.split("\n");
+ int[] index = new int[] { 0 };
+ SimpleElement element = null;
+ while ((element = parseLines(lines, index)) != null) {
+ elements.add(element);
+ }
+ return elements.toArray(new SimpleElement[elements.size()]);
+ }
+
+ /**
+ * Parses one element from the input lines array, starting at the inOutIndex
+ * and updating the inOutIndex to match the next unread line on output.
+ */
+ private static SimpleElement parseLines(String[] lines, int[] inOutIndex) {
+ SimpleElement e = null;
+ int index = inOutIndex[0];
+ while (index < lines.length) {
+ String line = lines[index++];
+ String s = line.trim();
+ if (s.startsWith("{")) { //$NON-NLS-1$
+ if (e == null) {
+ // This is the element's header, it should have
+ // the format "key=value,key=value,..."
+ String version = null;
+ String fqcn = null;
+ String parent = null;
+ Rect bounds = null;
+ Rect pbounds = null;
+
+ for (String s2 : s.substring(1).split(",")) { //$NON-NLS-1$
+ int pos = s2.indexOf('=');
+ if (pos <= 0 || pos == s2.length() - 1) {
+ continue;
+ }
+ String key = s2.substring(0, pos).trim();
+ String value = s2.substring(pos + 1).trim();
+
+ if (key.equals("V")) { //$NON-NLS-1$
+ version = value;
+ if (!value.equals(FORMAT_VERSION)) {
+ // Wrong format version. Don't even try to process anything
+ // else and just give up everything.
+ inOutIndex[0] = index;
+ return null;
+ }
+
+ } else if (key.equals("N")) { //$NON-NLS-1$
+ fqcn = value;
+
+ } else if (key.equals("P")) { //$NON-NLS-1$
+ parent = value;
+
+ } else if (key.equals("R") || key.equals("Q")) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Parse the canvas bounds
+ String[] sb = value.split(" +"); //$NON-NLS-1$
+ if (sb != null && sb.length == 4) {
+ Rect r = null;
+ try {
+ r = new Rect();
+ r.x = Integer.parseInt(sb[0]);
+ r.y = Integer.parseInt(sb[1]);
+ r.w = Integer.parseInt(sb[2]);
+ r.h = Integer.parseInt(sb[3]);
+
+ if (key.equals("R")) {
+ bounds = r;
+ } else {
+ pbounds = r;
+ }
+ } catch (NumberFormatException ignore) {
+ }
+ }
+ }
+ }
+
+ // We need at least a valid name to recreate an element
+ if (version != null && fqcn != null && fqcn.length() > 0) {
+ e = new SimpleElement(fqcn, parent, bounds, pbounds);
+ }
+ } else {
+ // This is an inner element... need to parse the { line again.
+ inOutIndex[0] = index - 1;
+ SimpleElement e2 = SimpleElement.parseLines(lines, inOutIndex);
+ if (e2 != null) {
+ e.addInnerElement(e2);
+ }
+ index = inOutIndex[0];
+ }
+
+ } else if (e != null && s.startsWith("@")) { //$NON-NLS-1$
+ SimpleAttribute a = SimpleAttribute.parseString(line);
+ if (a != null) {
+ e.addAttribute(a);
+ }
+
+ } else if (e != null && s.startsWith("}")) { //$NON-NLS-1$
+ // We're done with this element
+ inOutIndex[0] = index;
+ return e;
+ }
+ }
+ inOutIndex[0] = index;
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof SimpleElement) {
+ SimpleElement se = (SimpleElement) obj;
+
+ // Bounds and parentFqcn must be null on both sides or equal.
+ if ((mBounds == null && se.mBounds != null) ||
+ (mBounds != null && !mBounds.equals(se.mBounds))) {
+ return false;
+ }
+ if ((mParentFqcn == null && se.mParentFqcn != null) ||
+ (mParentFqcn != null && !mParentFqcn.equals(se.mParentFqcn))) {
+ return false;
+ }
+ if ((mParentBounds == null && se.mParentBounds != null) ||
+ (mParentBounds != null && !mParentBounds.equals(se.mParentBounds))) {
+ return false;
+ }
+
+ return mFqcn.equals(se.mFqcn) &&
+ mAttributes.size() == se.mAttributes.size() &&
+ mElements.size() == se.mElements.size() &&
+ mAttributes.equals(se.mAttributes) &&
+ mElements.equals(se.mElements);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ long c = mFqcn.hashCode();
+ // uses the formula defined in java.util.List.hashCode()
+ c = 31*c + mAttributes.hashCode();
+ c = 31*c + mElements.hashCode();
+ if (mParentFqcn != null) {
+ c = 31*c + mParentFqcn.hashCode();
+ }
+ if (mBounds != null && mBounds.isValid()) {
+ c = 31*c + mBounds.hashCode();
+ }
+ if (mParentBounds != null && mParentBounds.isValid()) {
+ c = 31*c + mParentBounds.hashCode();
+ }
+
+ if (c > 0x0FFFFFFFFL) {
+ // wrap any overflow
+ c = c ^ (c >> 32);
+ }
+ return (int)(c & 0x0FFFFFFFFL);
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java
new file mode 100644
index 000000000..20ac2033e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+
+import org.eclipse.swt.dnd.ByteArrayTransfer;
+import org.eclipse.swt.dnd.TransferData;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A d'n'd {@link Transfer} class that can transfer a <em>simplified</em> XML fragment
+ * to transfer elements and their attributes between {@link LayoutCanvas}.
+ * <p/>
+ * The implementation is based on the {@link ByteArrayTransfer} and what we transfer
+ * is text with the following fixed format:
+ * <p/>
+ * <pre>
+ * {element-name element-property ...
+ * attrib_name="attrib_value"
+ * attrib2="..."
+ * {...inner elements...
+ * }
+ * }
+ * {...next element...
+ * }
+ *
+ * </pre>
+ * The format has nothing to do with XML per se, except for the fact that the
+ * transfered content represents XML elements and XML attributes.
+ *
+ * <p/>
+ * The detailed syntax is:
+ * <pre>
+ * - ELEMENT := {NAME PROPERTY*\nATTRIB_LINE*ELEMENT*}\n
+ * - PROPERTY := $[A-Z]=[^ ]*
+ * - NAME := [^\n=]+
+ * - ATTRIB_LINE := @URI:NAME=[^\n]*\n
+ * </pre>
+ *
+ * Elements are represented by {@link SimpleElement}s and their attributes by
+ * {@link SimpleAttribute}s, all of which have very specific properties that are
+ * specifically limited to our needs for drag'n'drop.
+ */
+final class SimpleXmlTransfer extends ByteArrayTransfer {
+
+ // Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
+
+ private static final String TYPE_NAME = "android.ADT.simple.xml.transfer.1"; //$NON-NLS-1$
+ private static final int TYPE_ID = registerType(TYPE_NAME);
+ private static final SimpleXmlTransfer sInstance = new SimpleXmlTransfer();
+
+ /** Private constructor. Use {@link #getInstance()} to retrieve the singleton instance. */
+ private SimpleXmlTransfer() {
+ // pass
+ }
+
+ /** Returns the singleton instance. */
+ public static SimpleXmlTransfer getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Helper method that returns the FQCN transfered for the given {@link ElementDescriptor}.
+ * <p/>
+ * If the descriptor is a {@link ViewElementDescriptor}, the transfered data is the FQCN
+ * of the Android View class represented (e.g. "android.widget.Button").
+ * For any other non-null descriptor, the XML name is used.
+ * Otherwise it is null.
+ *
+ * @param desc The {@link ElementDescriptor} to transfer.
+ * @return The FQCN, XML name or null.
+ */
+ public static String getFqcn(ElementDescriptor desc) {
+ if (desc instanceof ViewElementDescriptor) {
+ return ((ViewElementDescriptor) desc).getFullClassName();
+ } else if (desc != null) {
+ return desc.getXmlName();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected int[] getTypeIds() {
+ return new int[] { TYPE_ID };
+ }
+
+ @Override
+ protected String[] getTypeNames() {
+ return new String[] { TYPE_NAME };
+ }
+
+ /** Transforms a array of {@link SimpleElement} into a native data transfer. */
+ @Override
+ protected void javaToNative(Object object, TransferData transferData) {
+ if (object == null || !(object instanceof SimpleElement[])) {
+ return;
+ }
+
+ if (isSupportedType(transferData)) {
+ StringBuilder sb = new StringBuilder();
+ for (SimpleElement e : (SimpleElement[]) object) {
+ sb.append(e.toString());
+ }
+ String data = sb.toString();
+
+ try {
+ byte[] buf = data.getBytes("UTF-8"); //$NON-NLS-1$
+ super.javaToNative(buf, transferData);
+ } catch (UnsupportedEncodingException e) {
+ // unlikely; ignore
+ }
+ }
+ }
+
+ /**
+ * Recreates an array of {@link SimpleElement} from a native data transfer.
+ *
+ * @return An array of {@link SimpleElement} or null. The array may be empty.
+ */
+ @Override
+ protected Object nativeToJava(TransferData transferData) {
+ if (isSupportedType(transferData)) {
+ byte[] buf = (byte[]) super.nativeToJava(transferData);
+ if (buf != null && buf.length > 0) {
+ try {
+ String s = new String(buf, "UTF-8"); //$NON-NLS-1$
+ return SimpleElement.parseString(s);
+ } catch (UnsupportedEncodingException e) {
+ // unlikely to happen, but still possible
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java
new file mode 100644
index 000000000..0923dda79
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java
@@ -0,0 +1,75 @@
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuCreator;
+import org.eclipse.swt.events.MenuEvent;
+import org.eclipse.swt.events.MenuListener;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+
+/**
+ * Action which creates a submenu that is dynamically populated by subclasses
+ */
+public abstract class SubmenuAction extends Action implements MenuListener, IMenuCreator {
+ private Menu mMenu;
+
+ public SubmenuAction(String title) {
+ super(title, IAction.AS_DROP_DOWN_MENU);
+ }
+
+ @Override
+ public IMenuCreator getMenuCreator() {
+ return this;
+ }
+
+ @Override
+ public void dispose() {
+ if (mMenu != null) {
+ mMenu.dispose();
+ mMenu = null;
+ }
+ }
+
+ @Override
+ public Menu getMenu(Control parent) {
+ return null;
+ }
+
+ @Override
+ public Menu getMenu(Menu parent) {
+ mMenu = new Menu(parent);
+ mMenu.addMenuListener(this);
+ return mMenu;
+ }
+
+ @Override
+ public void menuHidden(MenuEvent e) {
+ }
+
+ protected abstract void addMenuItems(Menu menu);
+
+ @Override
+ public void menuShown(MenuEvent e) {
+ // TODO: Replace this stuff with manager.setRemoveAllWhenShown(true);
+ MenuItem[] menuItems = mMenu.getItems();
+ for (int i = 0; i < menuItems.length; i++) {
+ menuItems[i].dispose();
+ }
+ addMenuItems(mMenu);
+ }
+
+ protected void addDisabledMessageItem(String message) {
+ IAction action = new Action(message, IAction.AS_PUSH_BUTTON) {
+ @Override
+ public void run() {
+ }
+ };
+ action.setEnabled(false);
+ new ActionContributionItem(action).fill(mMenu, -1);
+
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java
new file mode 100644
index 000000000..93a33283c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.DrawingStyle;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.RGB;
+
+/**
+ * Description of the drawing styles with specific color, line style and alpha
+ * definitions. This class corresponds to the more generic {@link DrawingStyle}
+ * class which defines the drawing styles but does not introduce any specific
+ * SWT values to the API clients.
+ * <p>
+ * TODO: This class should eventually be replaced by a scheme where the color
+ * constants are instead coming from the theme.
+ */
+public enum SwtDrawingStyle {
+ /**
+ * The style definition corresponding to {@link DrawingStyle#SELECTION}
+ */
+ SELECTION(new RGB(0x00, 0x99, 0xFF), 192, new RGB(0x00, 0x99, 0xFF), 192, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GUIDELINE}
+ */
+ GUIDELINE(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GUIDELINE}
+ */
+ GUIDELINE_SHADOW(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GUIDELINE_DASHED}
+ */
+ GUIDELINE_DASHED(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_CUSTOM),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DISTANCE}
+ */
+ DISTANCE(new RGB(0xFF, 0x00, 0x00), 192 - 32, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GRID}
+ */
+ GRID(new RGB(0xAA, 0xAA, 0xAA), 128, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#HOVER}
+ */
+ HOVER(null, 0, new RGB(0xFF, 0xFF, 0xFF), 40, 1, SWT.LINE_DOT),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#HOVER}
+ */
+ HOVER_SELECTION(null, 0, new RGB(0xFF, 0xFF, 0xFF), 10, 1, SWT.LINE_DOT),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#ANCHOR}
+ */
+ ANCHOR(new RGB(0x00, 0x99, 0xFF), 96, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#OUTLINE}
+ */
+ OUTLINE(new RGB(0x88, 0xFF, 0x88), 160, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DROP_RECIPIENT}
+ */
+ DROP_RECIPIENT(new RGB(0xFF, 0x99, 0x00), 255, new RGB(0xFF, 0x99, 0x00), 160, 2,
+ SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DROP_ZONE}
+ */
+ DROP_ZONE(new RGB(0x00, 0xAA, 0x00), 220, new RGB(0x55, 0xAA, 0x00), 200, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to
+ * {@link DrawingStyle#DROP_ZONE_ACTIVE}
+ */
+ DROP_ZONE_ACTIVE(new RGB(0x00, 0xAA, 0x00), 220, new RGB(0x00, 0xAA, 0x00), 128, 2,
+ SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DROP_PREVIEW}
+ */
+ DROP_PREVIEW(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_CUSTOM),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#RESIZE_PREVIEW}
+ */
+ RESIZE_PREVIEW(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style used to show a proposed resize bound which is being rejected (for example,
+ * because there is no near edge to attach to in a RelativeLayout).
+ */
+ RESIZE_FAIL(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_CUSTOM),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#HELP}
+ */
+ HELP(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0x00, 0x00, 0x00), 128, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#INVALID}
+ */
+ INVALID(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0x00, 0x00), 64, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DEPENDENCY}
+ */
+ DEPENDENCY(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0xFF, 0x00), 24, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#CYCLE}
+ */
+ CYCLE(new RGB(0xFF, 0x00, 0x00), 192, null, 0, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DRAGGED}
+ */
+ DRAGGED(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0x00, 0xFF, 0x00), 16, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#EMPTY}
+ */
+ EMPTY(new RGB(0xFF, 0xFF, 0x55), 255, new RGB(0xFF, 0xFF, 0x55), 255, 1, SWT.LINE_DASH),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#CUSTOM1}
+ */
+ CUSTOM1(new RGB(0xFF, 0x00, 0xFF), 255, null, 0, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#CUSTOM2}
+ */
+ CUSTOM2(new RGB(0x00, 0xFF, 0xFF), 255, null, 0, 1, SWT.LINE_DOT);
+
+ /**
+ * Construct a new style value with the given foreground, background, width,
+ * linestyle and transparency.
+ *
+ * @param stroke A color descriptor for the foreground color, or null if no
+ * foreground color should be set
+ * @param fill A color descriptor for the background color, or null if no
+ * foreground color should be set
+ * @param lineWidth The line width, in pixels, or 0 if no line width should
+ * be set
+ * @param lineStyle The SWT line style - such as {@link SWT#LINE_SOLID}.
+ * @param strokeAlpha The alpha value of the stroke, an integer in the range 0 to 255
+ * where 0 is fully transparent and 255 is fully opaque.
+ * @param fillAlpha The alpha value of the fill, an integer in the range 0 to 255
+ * where 0 is fully transparent and 255 is fully opaque.
+ */
+ private SwtDrawingStyle(RGB stroke, int strokeAlpha, RGB fill, int fillAlpha, int lineWidth,
+ int lineStyle) {
+ mStroke = stroke;
+ mFill = fill;
+ mLineWidth = lineWidth;
+ mLineStyle = lineStyle;
+ mStrokeAlpha = strokeAlpha;
+ mFillAlpha = fillAlpha;
+ }
+
+ /**
+ * Convenience constructor for typical drawing styles, which do not specify
+ * a fill and use a standard thickness line
+ *
+ * @param stroke Stroke color to be used (e.g. for the border/foreground)
+ * @param strokeAlpha Transparency to use for the stroke; 0 is transparent
+ * and 255 is fully opaque.
+ * @param lineStyle The SWT line style - such as {@link SWT#LINE_SOLID}.
+ */
+ private SwtDrawingStyle(RGB stroke, int strokeAlpha, int lineStyle) {
+ this(stroke, strokeAlpha, null, 255, 1, lineStyle);
+ }
+
+ /**
+ * Return the stroke/foreground/border RGB color description to be used for
+ * this style, or null if none
+ */
+ public RGB getStrokeColor() {
+ return mStroke;
+ }
+
+ /**
+ * Return the fill/background/interior RGB color description to be used for
+ * this style, or null if none
+ */
+ public RGB getFillColor() {
+ return mFill;
+ }
+
+ /** Return the line width to be used for this style */
+ public int getLineWidth() {
+ return mLineWidth;
+ }
+
+ /** Return the SWT line style to be used for this style */
+ public int getLineStyle() {
+ return mLineStyle;
+ }
+
+ /**
+ * Return the stroke alpha value (in the range 0,255) to be used for this
+ * style
+ */
+ public int getStrokeAlpha() {
+ return mStrokeAlpha;
+ }
+
+ /**
+ * Return the fill alpha value (in the range 0,255) to be used for this
+ * style
+ */
+ public int getFillAlpha() {
+ return mFillAlpha;
+ }
+
+ /**
+ * Return the corresponding SwtDrawingStyle for the given
+ * {@link DrawingStyle}
+ * @param style The style to convert from a {@link DrawingStyle} to a {@link SwtDrawingStyle}.
+ * @return A corresponding {@link SwtDrawingStyle}.
+ */
+ public static SwtDrawingStyle of(DrawingStyle style) {
+ switch (style) {
+ case SELECTION:
+ return SELECTION;
+ case GUIDELINE:
+ return GUIDELINE;
+ case GUIDELINE_SHADOW:
+ return GUIDELINE_SHADOW;
+ case GUIDELINE_DASHED:
+ return GUIDELINE_DASHED;
+ case DISTANCE:
+ return DISTANCE;
+ case GRID:
+ return GRID;
+ case HOVER:
+ return HOVER;
+ case HOVER_SELECTION:
+ return HOVER_SELECTION;
+ case ANCHOR:
+ return ANCHOR;
+ case OUTLINE:
+ return OUTLINE;
+ case DROP_ZONE:
+ return DROP_ZONE;
+ case DROP_ZONE_ACTIVE:
+ return DROP_ZONE_ACTIVE;
+ case DROP_RECIPIENT:
+ return DROP_RECIPIENT;
+ case DROP_PREVIEW:
+ return DROP_PREVIEW;
+ case RESIZE_PREVIEW:
+ return RESIZE_PREVIEW;
+ case RESIZE_FAIL:
+ return RESIZE_FAIL;
+ case HELP:
+ return HELP;
+ case INVALID:
+ return INVALID;
+ case DEPENDENCY:
+ return DEPENDENCY;
+ case CYCLE:
+ return CYCLE;
+ case DRAGGED:
+ return DRAGGED;
+ case EMPTY:
+ return EMPTY;
+ case CUSTOM1:
+ return CUSTOM1;
+ case CUSTOM2:
+ return CUSTOM2;
+
+ // Internal error
+ default:
+ throw new IllegalArgumentException("Unknown style " + style);
+ }
+ }
+
+ /** RGB description of the stroke/foreground/border color */
+ private final RGB mStroke;
+
+ /** RGB description of the fill/foreground/interior color */
+ private final RGB mFill;
+
+ /** Pixel thickness of the stroke/border */
+ private final int mLineWidth;
+
+ /** SWT line style of the border/stroke */
+ private final int mLineStyle;
+
+ /** Alpha (in the range 0-255) of the stroke/border */
+ private final int mStrokeAlpha;
+
+ /** Alpha (in the range 0-255) of the fill/interior */
+ private final int mFillAlpha;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java
new file mode 100644
index 000000000..64e91bedf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.util.List;
+
+/**
+ * Various generic SWT utilities such as image conversion.
+ */
+public class SwtUtils {
+
+ private SwtUtils() {
+ }
+
+ /**
+ * Returns the {@link PaletteData} describing the ARGB ordering expected from integers
+ * representing pixels for AWT {@link BufferedImage}.
+ *
+ * @param imageType the {@link BufferedImage#getType()} type
+ * @return A new {@link PaletteData} suitable for AWT images.
+ */
+ public static PaletteData getAwtPaletteData(int imageType) {
+ switch (imageType) {
+ case BufferedImage.TYPE_INT_RGB:
+ case BufferedImage.TYPE_INT_ARGB:
+ case BufferedImage.TYPE_INT_ARGB_PRE:
+ return new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF);
+
+ case BufferedImage.TYPE_3BYTE_BGR:
+ case BufferedImage.TYPE_4BYTE_ABGR:
+ case BufferedImage.TYPE_4BYTE_ABGR_PRE:
+ return new PaletteData(0x000000FF, 0x0000FF00, 0x00FF0000);
+
+ default:
+ throw new UnsupportedOperationException("RGB type not supported yet.");
+ }
+ }
+
+ /**
+ * Returns true if the given type of {@link BufferedImage} is supported for
+ * conversion. For unsupported formats, use
+ * {@link #convertToCompatibleFormat(BufferedImage)} first.
+ *
+ * @param imageType the {@link BufferedImage#getType()}
+ * @return true if we can convert the given buffered image format
+ */
+ private static boolean isSupportedPaletteType(int imageType) {
+ switch (imageType) {
+ case BufferedImage.TYPE_INT_RGB:
+ case BufferedImage.TYPE_INT_ARGB:
+ case BufferedImage.TYPE_INT_ARGB_PRE:
+ case BufferedImage.TYPE_3BYTE_BGR:
+ case BufferedImage.TYPE_4BYTE_ABGR:
+ case BufferedImage.TYPE_4BYTE_ABGR_PRE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** Converts the given arbitrary {@link BufferedImage} to another {@link BufferedImage}
+ * in a format that is supported (see {@link #isSupportedPaletteType(int)})
+ *
+ * @param image the image to be converted
+ * @return a new image that is in a guaranteed compatible format
+ */
+ private static BufferedImage convertToCompatibleFormat(BufferedImage image) {
+ BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics graphics = converted.getGraphics();
+ graphics.drawImage(image, 0, 0, null);
+ graphics.dispose();
+
+ return converted;
+ }
+
+ /**
+ * Converts an AWT {@link BufferedImage} into an equivalent SWT {@link Image}. Whether
+ * the transparency data is transferred is optional, and this method can also apply an
+ * alpha adjustment during the conversion.
+ * <p/>
+ * Implementation details: on Windows, the returned {@link Image} will have an ordering
+ * matching the Windows DIB (e.g. RGBA, not ARGB). Callers must make sure to use
+ * <code>Image.getImageData().paletteData</code> to get the right pixels out of the image.
+ *
+ * @param display The display where the SWT image will be shown
+ * @param awtImage The AWT {@link BufferedImage}
+ * @param transferAlpha If true, copy alpha data out of the source image
+ * @param globalAlpha If -1, do nothing, otherwise adjust the alpha of the final image
+ * by the given amount in the range [0,255]
+ * @return A new SWT {@link Image} with the same contents as the source
+ * {@link BufferedImage}
+ */
+ public static Image convertToSwt(Device display, BufferedImage awtImage,
+ boolean transferAlpha, int globalAlpha) {
+ if (!isSupportedPaletteType(awtImage.getType())) {
+ awtImage = convertToCompatibleFormat(awtImage);
+ }
+
+ int width = awtImage.getWidth();
+ int height = awtImage.getHeight();
+
+ WritableRaster raster = awtImage.getRaster();
+ DataBuffer dataBuffer = raster.getDataBuffer();
+ ImageData imageData =
+ new ImageData(width, height, 32, getAwtPaletteData(awtImage.getType()));
+
+ if (dataBuffer instanceof DataBufferInt) {
+ int[] imageDataBuffer = ((DataBufferInt) dataBuffer).getData();
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+ } else if (dataBuffer instanceof DataBufferByte) {
+ byte[] imageDataBuffer = ((DataBufferByte) dataBuffer).getData();
+ try {
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+ } catch (SWTException se) {
+ // Unsupported depth
+ return convertToSwt(display, convertToCompatibleFormat(awtImage),
+ transferAlpha, globalAlpha);
+ }
+ }
+
+ if (transferAlpha) {
+ byte[] alphaData = new byte[height * width];
+ for (int y = 0; y < height; y++) {
+ byte[] alphaRow = new byte[width];
+ for (int x = 0; x < width; x++) {
+ int alpha = awtImage.getRGB(x, y) >>> 24;
+
+ // We have to multiply in the alpha now since if we
+ // set ImageData.alpha, it will ignore the alphaData.
+ if (globalAlpha != -1) {
+ alpha = alpha * globalAlpha >> 8;
+ }
+
+ alphaRow[x] = (byte) alpha;
+ }
+ System.arraycopy(alphaRow, 0, alphaData, y * width, width);
+ }
+
+ imageData.alphaData = alphaData;
+ } else if (globalAlpha != -1) {
+ imageData.alpha = globalAlpha;
+ }
+
+ return new Image(display, imageData);
+ }
+
+ /**
+ * Converts a direct-color model SWT image to an equivalent AWT image. If the image
+ * does not have a supported color model, returns null. This method does <b>NOT</b>
+ * preserve alpha in the source image.
+ *
+ * @param swtImage the SWT image to be converted to AWT
+ * @return an AWT image representing the source SWT image
+ */
+ public static BufferedImage convertToAwt(Image swtImage) {
+ ImageData swtData = swtImage.getImageData();
+ BufferedImage awtImage =
+ new BufferedImage(swtData.width, swtData.height, BufferedImage.TYPE_INT_ARGB);
+ PaletteData swtPalette = swtData.palette;
+ if (swtPalette.isDirect) {
+ PaletteData awtPalette = getAwtPaletteData(awtImage.getType());
+
+ if (swtPalette.equals(awtPalette)) {
+ // No color conversion needed.
+ for (int y = 0; y < swtData.height; y++) {
+ for (int x = 0; x < swtData.width; x++) {
+ int pixel = swtData.getPixel(x, y);
+ awtImage.setRGB(x, y, 0xFF000000 | pixel);
+ }
+ }
+ } else {
+ // We need to remap the colors
+ int sr = -awtPalette.redShift + swtPalette.redShift;
+ int sg = -awtPalette.greenShift + swtPalette.greenShift;
+ int sb = -awtPalette.blueShift + swtPalette.blueShift;
+
+ for (int y = 0; y < swtData.height; y++) {
+ for (int x = 0; x < swtData.width; x++) {
+ int pixel = swtData.getPixel(x, y);
+
+ int r = pixel & swtPalette.redMask;
+ int g = pixel & swtPalette.greenMask;
+ int b = pixel & swtPalette.blueMask;
+ r = (sr < 0) ? r >>> -sr : r << sr;
+ g = (sg < 0) ? g >>> -sg : g << sg;
+ b = (sb < 0) ? b >>> -sb : b << sb;
+
+ pixel = 0xFF000000 | r | g | b;
+ awtImage.setRGB(x, y, pixel);
+ }
+ }
+ }
+ } else {
+ return null;
+ }
+
+ return awtImage;
+ }
+
+ /**
+ * Creates a new image from a source image where the contents from a given set of
+ * bounding boxes are copied into the new image and the rest is left transparent. A
+ * scale can be applied to make the resulting image larger or smaller than the source
+ * image. Note that the alpha channel in the original image is ignored, and the alpha
+ * values for the painted rectangles will be set to a specific value passed into this
+ * function.
+ *
+ * @param image the source image
+ * @param rectangles the set of rectangles (bounding boxes) to copy from the source
+ * image
+ * @param boundingBox the bounding rectangle of the rectangle list, which can be
+ * computed by {@link ImageUtils#getBoundingRectangle}
+ * @param scale a scale factor to apply to the result, e.g. 0.5 to shrink the
+ * destination down 50%, 1.0 to leave it alone and 2.0 to zoom in by
+ * doubling the image size
+ * @param alpha the alpha (in the range 0-255) that painted bits should be set to
+ * @return a pair of the rendered cropped image, and the location within the source
+ * image that the crop begins (multiplied by the scale). May return null if
+ * there are no selected items.
+ */
+ public static Image drawRectangles(Image image,
+ List<Rectangle> rectangles, Rectangle boundingBox, double scale, byte alpha) {
+
+ if (rectangles.size() == 0 || boundingBox == null || boundingBox.isEmpty()) {
+ return null;
+ }
+
+ ImageData srcData = image.getImageData();
+ int destWidth = (int) (scale * boundingBox.width);
+ int destHeight = (int) (scale * boundingBox.height);
+
+ ImageData destData = new ImageData(destWidth, destHeight, srcData.depth, srcData.palette);
+ byte[] alphaData = new byte[destHeight * destWidth];
+ destData.alphaData = alphaData;
+
+ for (Rectangle bounds : rectangles) {
+ int dx1 = bounds.x - boundingBox.x;
+ int dy1 = bounds.y - boundingBox.y;
+ int dx2 = dx1 + bounds.width;
+ int dy2 = dy1 + bounds.height;
+
+ dx1 *= scale;
+ dy1 *= scale;
+ dx2 *= scale;
+ dy2 *= scale;
+
+ int sx1 = bounds.x;
+ int sy1 = bounds.y;
+ int sx2 = sx1 + bounds.width;
+ int sy2 = sy1 + bounds.height;
+
+ if (scale == 1.0d) {
+ for (int dy = dy1, sy = sy1; dy < dy2; dy++, sy++) {
+ for (int dx = dx1, sx = sx1; dx < dx2; dx++, sx++) {
+ destData.setPixel(dx, dy, srcData.getPixel(sx, sy));
+ alphaData[dy * destWidth + dx] = alpha;
+ }
+ }
+ } else {
+ // Scaled copy.
+ int sxDelta = sx2 - sx1;
+ int dxDelta = dx2 - dx1;
+ int syDelta = sy2 - sy1;
+ int dyDelta = dy2 - dy1;
+ for (int dy = dy1, sy = sy1; dy < dy2; dy++, sy = (dy - dy1) * syDelta / dyDelta
+ + sy1) {
+ for (int dx = dx1, sx = sx1; dx < dx2; dx++, sx = (dx - dx1) * sxDelta
+ / dxDelta + sx1) {
+ assert sx < sx2 && sy < sy2;
+ destData.setPixel(dx, dy, srcData.getPixel(sx, sy));
+ alphaData[dy * destWidth + dx] = alpha;
+ }
+ }
+ }
+ }
+
+ return new Image(image.getDevice(), destData);
+ }
+
+ /**
+ * Creates a new empty/blank image of the given size
+ *
+ * @param display the display to associate the image with
+ * @param width the width of the image
+ * @param height the height of the image
+ * @return a new blank image of the given size
+ */
+ public static Image createEmptyImage(Display display, int width, int height) {
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ return SwtUtils.convertToSwt(display, image, false, 0);
+ }
+
+ /**
+ * Converts the given SWT {@link Rectangle} into an ADT {@link Rect}
+ *
+ * @param swtRect the SWT {@link Rectangle}
+ * @return an equivalent {@link Rect}
+ */
+ public static Rect toRect(Rectangle swtRect) {
+ return new Rect(swtRect.x, swtRect.y, swtRect.width, swtRect.height);
+ }
+
+ /**
+ * Sets the values of the given ADT {@link Rect} to the values of the given SWT
+ * {@link Rectangle}
+ *
+ * @param target the ADT {@link Rect} to modify
+ * @param source the SWT {@link Rectangle} to read values from
+ */
+ public static void set(Rect target, Rectangle source) {
+ target.set(source.x, source.y, source.width, source.height);
+ }
+
+ /**
+ * Compares an ADT {@link Rect} with an SWT {@link Rectangle} and returns true if they
+ * are equivalent
+ *
+ * @param r1 the ADT {@link Rect}
+ * @param r2 the SWT {@link Rectangle}
+ * @return true if the two rectangles are equivalent
+ */
+ public static boolean equals(Rect r1, Rectangle r2) {
+ return r1.x == r2.x && r1.y == r2.y && r1.w == r2.width && r1.h == r2.height;
+
+ }
+
+ /**
+ * Get the average width of the font used by the given control
+ *
+ * @param display the display associated with the font usage
+ * @param font the font to look up the average character width for
+ * @return the average width, in pixels, of the given font
+ */
+ public static final int getAverageCharWidth(Display display, Font font) {
+ GC gc = new GC(display);
+ gc.setFont(font);
+ FontMetrics fontMetrics = gc.getFontMetrics();
+ int width = fontMetrics.getAverageCharWidth();
+ gc.dispose();
+ return width;
+ }
+
+ /**
+ * Get the average width of the given font
+ *
+ * @param control the control to look up the default font for
+ * @return the average width, in pixels, of the current font in the control
+ */
+ public static final int getAverageCharWidth(Control control) {
+ GC gc = new GC(control.getDisplay());
+ int width = gc.getFontMetrics().getAverageCharWidth();
+ gc.dispose();
+ return width;
+ }
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * <p>
+ * This corresponds to {@link ImageUtils#drawRectangleShadow(Graphics, int, int, int, int)},
+ * but applied directly to an SWT graphics context instead, such that no image conversion
+ * has to be performed.
+ * <p>
+ * Make sure to keep changes in the visual appearance here in sync with the
+ * AWT version in {@link ImageUtils#drawRectangleShadow(Graphics, int, int, int, int)}.
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawRectangleShadow(GC gc, int x, int y, int width, int height) {
+ if (sShadowBottomLeft == null) {
+ IconFactory icons = IconFactory.getInstance();
+ // See ImageUtils.drawRectangleShadow for an explanation of the assets.
+ sShadowBottomLeft = icons.getIcon("shadow-bl"); //$NON-NLS-1$
+ sShadowBottom = icons.getIcon("shadow-b"); //$NON-NLS-1$
+ sShadowBottomRight = icons.getIcon("shadow-br"); //$NON-NLS-1$
+ sShadowRight = icons.getIcon("shadow-r"); //$NON-NLS-1$
+ sShadowTopRight = icons.getIcon("shadow-tr"); //$NON-NLS-1$
+ assert sShadowBottomRight.getImageData().width == SHADOW_SIZE;
+ assert sShadowBottomRight.getImageData().height == SHADOW_SIZE;
+ }
+
+ ImageData bottomLeftData = sShadowBottomLeft.getImageData();
+ ImageData topRightData = sShadowTopRight.getImageData();
+ ImageData bottomData = sShadowBottom.getImageData();
+ ImageData rightData = sShadowRight.getImageData();
+ int blWidth = bottomLeftData.width;
+ int trHeight = topRightData.height;
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(sShadowBottomLeft, x, y + height);
+ gc.drawImage(sShadowBottomRight, x + width, y + height);
+ gc.drawImage(sShadowTopRight, x + width, y);
+ gc.drawImage(sShadowBottom,
+ 0, 0,
+ bottomData.width, bottomData.height,
+ x + bottomLeftData.width, y + height,
+ width - bottomLeftData.width, bottomData.height);
+ gc.drawImage(sShadowRight,
+ 0, 0,
+ rightData.width, rightData.height,
+ x + width, y + topRightData.height,
+ rightData.width, height - topRightData.height);
+ }
+
+ private static Image sShadowBottomLeft;
+ private static Image sShadowBottom;
+ private static Image sShadowBottomRight;
+ private static Image sShadowRight;
+ private static Image sShadowTopRight;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
new file mode 100644
index 000000000..d247e28d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
@@ -0,0 +1,771 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.RandomAccess;
+import java.util.Set;
+
+/**
+ * The view hierarchy class manages a set of view info objects and performs find
+ * operations on this set.
+ */
+public class ViewHierarchy {
+ private static final boolean DUMP_INFO = false;
+
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ViewHierarchy} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
+ * for.
+ */
+ public ViewHierarchy(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * The CanvasViewInfo root created by the last call to {@link #setSession}
+ * with a valid layout.
+ * <p/>
+ * This <em>can</em> be null to indicate we're dealing with an empty document with
+ * no root node. Null here does not mean the result was invalid, merely that the XML
+ * had no content to display -- we need to treat an empty document as valid so that
+ * we can drop new items in it.
+ */
+ private CanvasViewInfo mLastValidViewInfoRoot;
+
+ /**
+ * True when the last {@link #setSession} provided a valid {@link LayoutScene}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
+ * valid since it is an acceptable drop target.
+ */
+ private boolean mIsResultValid;
+
+ /**
+ * A list of invisible parents (see {@link CanvasViewInfo#isInvisible()} for
+ * details) in the current view hierarchy.
+ */
+ private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>();
+
+ /**
+ * A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it
+ * reflects updates to the underlying {@link #mInvisibleParents} list.
+ */
+ private final List<CanvasViewInfo> mInvisibleParentsReadOnly =
+ Collections.unmodifiableList(mInvisibleParents);
+
+ /**
+ * Flag which records whether or not we have any exploded parent nodes in this
+ * view hierarchy. This is used to track whether or not we need to recompute the
+ * layout when we exit show-all-invisible-parents mode (see
+ * {@link LayoutCanvas#showInvisibleViews}).
+ */
+ private boolean mExplodedParents;
+
+ /**
+ * Bounds of included views in the current view hierarchy when rendered in other context
+ */
+ private List<Rectangle> mIncludedBounds;
+
+ /** The render session for the current view hierarchy */
+ private RenderSession mSession;
+
+ /** Map from nodes to canvas view infos */
+ private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap();
+
+ /** Map from DOM nodes to canvas view infos */
+ private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap();
+
+ /**
+ * Disposes the view hierarchy content.
+ */
+ public void dispose() {
+ if (mSession != null) {
+ mSession.dispose();
+ mSession = null;
+ }
+ }
+
+
+ /**
+ * Sets the result of the layout rendering. The result object indicates if the layout
+ * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
+ *
+ * Implementation detail: the bridge's computeLayout() method already returns a newly
+ * allocated ILayourResult. That means we can keep this result and hold on to it
+ * when it is valid.
+ *
+ * @param session The new session, either valid or not.
+ * @param explodedNodes The set of individual nodes the layout computer was asked to
+ * explode. Note that these are independent of the explode-all mode where
+ * all views are exploded; this is used only for the mode (
+ * {@link LayoutCanvas#showInvisibleViews}) where individual invisible
+ * nodes are padded during certain interactions.
+ */
+ /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
+ boolean layoutlib5) {
+ // replace the previous scene, so the previous scene must be disposed.
+ if (mSession != null) {
+ mSession.dispose();
+ }
+
+ mSession = session;
+ mIsResultValid = (session != null && session.getResult().isSuccess());
+ mExplodedParents = false;
+ mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50);
+ if (mIsResultValid && session != null) {
+ List<ViewInfo> rootList = session.getRootViews();
+
+ Pair<CanvasViewInfo,List<Rectangle>> infos = null;
+
+ if (rootList == null || rootList.size() == 0) {
+ // Special case: Look to see if this is really an empty <merge> view,
+ // which shows up without any ViewInfos in the merge. In that case we
+ // want to manufacture an empty view, such that we can target the view
+ // via drag & drop, etc.
+ if (hasMergeRoot()) {
+ ViewInfo mergeRoot = createMergeInfo(session);
+ infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
+ } else {
+ infos = null;
+ }
+ } else {
+ if (rootList.size() > 1 && hasMergeRoot()) {
+ ViewInfo mergeRoot = createMergeInfo(session);
+ mergeRoot.setChildren(rootList);
+ infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
+ } else {
+ ViewInfo root = rootList.get(0);
+
+ if (root != null) {
+ infos = CanvasViewInfo.create(root, layoutlib5);
+ if (DUMP_INFO) {
+ dump(session, root, 0);
+ }
+ } else {
+ infos = null;
+ }
+ }
+ }
+ if (infos != null) {
+ mLastValidViewInfoRoot = infos.getFirst();
+ mIncludedBounds = infos.getSecond();
+
+ if (mLastValidViewInfoRoot.getUiViewNode() == null &&
+ mLastValidViewInfoRoot.getChildren().isEmpty()) {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (editor.getIncludedWithin() != null) {
+ // Somehow, this view was supposed to be rendered within another
+ // view, yet this view was rendered as part of the other view.
+ // In that case, abort attempting to show included in; clear the
+ // include context and trigger a standalone re-render.
+ editor.showIn(null);
+ return;
+ }
+ }
+
+ } else {
+ mLastValidViewInfoRoot = null;
+ mIncludedBounds = null;
+ }
+
+ updateNodeProxies(mLastValidViewInfoRoot);
+
+ // Update the data structures related to tracking invisible and exploded nodes.
+ // We need to find the {@link CanvasViewInfo} objects that correspond to
+ // the passed in {@link UiElementNode} keys that were re-rendered, and mark
+ // them as exploded and store them in a list for rendering.
+ mExplodedParents = false;
+ mInvisibleParents.clear();
+ addInvisibleParents(mLastValidViewInfoRoot, explodedNodes);
+
+ mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size());
+ for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) {
+ mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue());
+ }
+
+ // Update the selection
+ mCanvas.getSelectionManager().sync();
+ } else {
+ mIncludedBounds = null;
+ mInvisibleParents.clear();
+ mDomNodeToView = Collections.emptyMap();
+ }
+ }
+
+ private ViewInfo createMergeInfo(RenderSession session) {
+ BufferedImage image = session.getImage();
+ ControlPoint imageSize = ControlPoint.create(mCanvas,
+ mCanvas.getHorizontalTransform().getMargin() + image.getWidth(),
+ mCanvas.getVerticalTransform().getMargin() + image.getHeight());
+ LayoutPoint layoutSize = imageSize.toLayout();
+ UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
+ List<UiElementNode> children = model.getUiChildren();
+ return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y);
+ }
+
+ /**
+ * Returns true if this view hierarchy corresponds to an editor that has a {@code
+ * <merge>} tag at the root
+ *
+ * @return true if there is a {@code <merge>} at the root of this editor's document
+ */
+ private boolean hasMergeRoot() {
+ UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
+ if (model != null) {
+ List<UiElementNode> children = model.getUiChildren();
+ if (children != null && children.size() > 0
+ && VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates or updates the node proxy for this canvas view info.
+ * <p/>
+ * Since proxies are reused, this will update the bounds of an existing proxy when the
+ * canvas is refreshed and a view changes position or size.
+ * <p/>
+ * This is a recursive call that updates the whole hierarchy starting at the given
+ * view info.
+ */
+ private void updateNodeProxies(CanvasViewInfo vi) {
+ if (vi == null) {
+ return;
+ }
+
+ UiViewElementNode key = vi.getUiViewNode();
+
+ if (key != null) {
+ mCanvas.getNodeFactory().create(vi);
+ mNodeToView.put(key, vi);
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ updateNodeProxies(child);
+ }
+ }
+
+ /**
+ * Make a pass over the view hierarchy and look for two things:
+ * <ol>
+ * <li>Invisible parents. These are nodes that can hold children and have empty
+ * bounds. These are then added to the {@link #mInvisibleParents} list.
+ * <li>Exploded nodes. These are nodes that were previously marked as invisible, and
+ * subsequently rendered by a recomputed layout. They now no longer have empty bounds,
+ * but should be specially marked via {@link CanvasViewInfo#setExploded} such that we
+ * for example in selection operations can determine if we need to recompute the
+ * layout.
+ * </ol>
+ *
+ * @param vi
+ * @param invisibleNodes
+ */
+ private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) {
+ if (vi == null) {
+ return;
+ }
+
+ if (vi.isInvisible()) {
+ mInvisibleParents.add(vi);
+ } else if (invisibleNodes != null) {
+ UiViewElementNode key = vi.getUiViewNode();
+
+ if (key != null && invisibleNodes.contains(key)) {
+ vi.setExploded(true);
+ mExplodedParents = true;
+ mInvisibleParents.add(vi);
+ }
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ addInvisibleParents(child, invisibleNodes);
+ }
+ }
+
+ /**
+ * Returns the current {@link RenderSession}.
+ * @return the session or null if none have been set.
+ */
+ public RenderSession getSession() {
+ return mSession;
+ }
+
+ /**
+ * Returns true when the last {@link #setSession} provided a valid
+ * {@link RenderSession}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #getRoot()}) is considered
+ * valid since it is an acceptable drop target.
+ * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
+ */
+ public boolean isValid() {
+ return mIsResultValid;
+ }
+
+ /**
+ * Returns true if the last valid content of the canvas represents an empty document.
+ * @return True if the last valid content of the canvas represents an empty document.
+ */
+ public boolean isEmpty() {
+ return mLastValidViewInfoRoot == null;
+ }
+
+ /**
+ * Returns true if we have parents in this hierarchy that are invisible (e.g. because
+ * they have no children and zero layout bounds).
+ *
+ * @return True if we have invisible parents.
+ */
+ public boolean hasInvisibleParents() {
+ return mInvisibleParents.size() > 0;
+ }
+
+ /**
+ * Returns true if we have views that were exploded during rendering
+ * @return True if we have exploded parents
+ */
+ public boolean hasExplodedParents() {
+ return mExplodedParents;
+ }
+
+ /** Locates and return any views that overlap the given selection rectangle.
+ * @param topLeft The top left corner of the selection rectangle.
+ * @param bottomRight The bottom right corner of the selection rectangle.
+ * @return A collection of {@link CanvasViewInfo} objects that overlap the
+ * rectangle.
+ */
+ public Collection<CanvasViewInfo> findWithin(
+ LayoutPoint topLeft,
+ LayoutPoint bottomRight) {
+ Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
+ - topLeft.x, bottomRight.y - topLeft.y);
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
+ return infos;
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private void addWithin(
+ CanvasViewInfo canvasViewInfo,
+ Rectangle canvasRectangle,
+ List<CanvasViewInfo> infos) {
+ if (canvasViewInfo == null) {
+ return;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (canvasRectangle.intersects(r)) {
+
+ // try to find a matching child first
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ addWithin(child, canvasRectangle, infos);
+ }
+
+ if (canvasViewInfo != mLastValidViewInfoRoot) {
+ infos.add(canvasViewInfo);
+ }
+ }
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable Node node) {
+ CanvasViewInfo vi = mDomNodeToView.get(node);
+
+ if (vi == null) {
+ if (node == null) {
+ return null;
+ } else if (node.getNodeType() == Node.TEXT_NODE) {
+ return mDomNodeToView.get(node.getParentNode());
+ } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
+ return mDomNodeToView.get(((Attr) node).getOwnerElement());
+ } else if (node.getNodeType() == Node.DOCUMENT_NODE) {
+ return mDomNodeToView.get(((Document) node).getDocumentElement());
+ }
+ }
+
+ return vi;
+ }
+
+ /**
+ * Tries to find the inner most child matching the given x,y coordinates in
+ * the view info sub-tree, starting at the last know view info root. This
+ * uses the potentially-expanded selection bounds.
+ * <p/>
+ * Returns null if not found or if there's no view info root.
+ *
+ * @param p The point at which to look for the deepest match in the view
+ * hierarchy
+ * @return A {@link CanvasViewInfo} that intersects the given point, or null
+ * if nothing was found.
+ */
+ public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot == null) {
+ return null;
+ }
+
+ return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return null;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+
+ // try to find a matching child first
+ // Iterate in REVERSE z order such that siblings on top
+ // are checked before earlier siblings (this matters in layouts like
+ // FrameLayout and in <merge> contexts where the views are sitting on top
+ // of each other and we want to select the same view as the one drawn
+ // on top of the others
+ List<CanvasViewInfo> children = canvasViewInfo.getChildren();
+ assert children instanceof RandomAccess;
+ for (int i = children.size() - 1; i >= 0; i--) {
+ CanvasViewInfo child = children.get(i);
+ CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ // if no children matched, this is the view that we're looking for
+ return canvasViewInfo;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of all the possible alternatives for a given view at the given
+ * position. This is used to build and manage the "alternate" selection that cycles
+ * around the parents or children of the currently selected element.
+ */
+ /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot != null) {
+ return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
+ * Please don't use directly.
+ */
+ private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
+ LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
+ Rectangle r;
+
+ if (outList == null) {
+ outList = new ArrayList<CanvasViewInfo>();
+
+ if (parent != null) {
+ // add the parent root only once
+ r = parent.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(parent);
+ }
+ }
+ }
+
+ if (parent != null && !parent.getChildren().isEmpty()) {
+ // then add all children that match the position
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(child);
+ }
+ }
+
+ // finally recurse in the children
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ findAltViewInfoAt_Recursive(p, child, outList);
+ }
+ }
+ }
+
+ return outList;
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ public CanvasViewInfo findViewInfoFor(INode node) {
+ return findViewInfoFor((NodeProxy) node);
+ }
+
+ /**
+ * Tries to find a child with the same view key in the view info sub-tree.
+ * Returns null if not found.
+ *
+ * @param viewKey The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) {
+ return mNodeToView.get(viewKey);
+ }
+
+ /**
+ * Tries to find a child with the given node proxy as the view key.
+ * Returns null if not found.
+ *
+ * @param proxy The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable NodeProxy proxy) {
+ if (proxy == null) {
+ return null;
+ }
+ return mNodeToView.get(proxy.getNode());
+ }
+
+ /**
+ * Returns a list of ALL ViewInfos (possibly excluding the root, depending
+ * on the parameter for that).
+ *
+ * @param includeRoot If true, include the root in the list, otherwise
+ * exclude it (but include all its children)
+ * @return A list of canvas view infos.
+ */
+ public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mIsResultValid && mLastValidViewInfoRoot != null) {
+ findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
+ }
+
+ return infos;
+ }
+
+ private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
+ boolean includeRoot) {
+ if (canvasViewInfo != null) {
+ if (includeRoot || !canvasViewInfo.isRoot()) {
+ result.add(canvasViewInfo);
+ }
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ findAllViewInfos(result, child, true);
+ }
+ }
+ }
+
+ /**
+ * Returns the root of the view hierarchy, if any (could be null, for example
+ * on rendering failure).
+ *
+ * @return The current view hierarchy, or null
+ */
+ public CanvasViewInfo getRoot() {
+ return mLastValidViewInfoRoot;
+ }
+
+ /**
+ * Returns a collection of views that have zero bounds and that correspond to empty
+ * parents. Note that the views may not actually have zero bounds; in particular, if
+ * they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the
+ * bounds of a shown invisible node. Therefore, this method returns the views that
+ * would be invisible in a real rendering of the scene.
+ *
+ * @return A collection of empty parent views.
+ */
+ public List<CanvasViewInfo> getInvisibleViews() {
+ return mInvisibleParentsReadOnly;
+ }
+
+ /**
+ * Returns the invisible nodes (the {@link UiElementNode} objects corresponding
+ * to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}.
+ * We are pulling out the nodes since they preserve their identity across layout
+ * rendering, and in particular we return it as a set such that the layout renderer
+ * can perform quick identity checks when looking up attribute values during the
+ * rendering process.
+ *
+ * @return A set of the invisible nodes.
+ */
+ public Set<UiElementNode> getInvisibleNodes() {
+ if (mInvisibleParents.size() == 0) {
+ return Collections.emptySet();
+ }
+
+ Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size());
+ for (CanvasViewInfo info : mInvisibleParents) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+
+ return nodes;
+ }
+
+ /**
+ * Returns the list of bounds for included views in the current view hierarchy. Can be null
+ * when there are no included views.
+ *
+ * @return a list of included view bounds, or null
+ */
+ public List<Rectangle> getIncludedBounds() {
+ return mIncludedBounds;
+ }
+
+ /**
+ * Returns a map of the default properties for the given view object in this session
+ *
+ * @param viewObject the object to look up the properties map for
+ * @return the map of properties, or null if not found
+ */
+ @Nullable
+ public Map<String, String> getDefaultProperties(@NonNull Object viewObject) {
+ if (mSession != null) {
+ return mSession.getDefaultProperties(viewObject);
+ }
+
+ return null;
+ }
+
+ /**
+ * Dumps a {@link ViewInfo} hierarchy to stdout
+ *
+ * @param session the corresponding session, if any
+ * @param info the {@link ViewInfo} object to dump
+ * @param depth the depth to indent it to
+ */
+ public static void dump(RenderSession session, ViewInfo info, int depth) {
+ if (DUMP_INFO) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ sb.append(" "); //$NON-NLS-1$
+ }
+ sb.append(info.getClassName());
+ sb.append(" ["); //$NON-NLS-1$
+ sb.append(info.getLeft());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getTop());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getRight());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getBottom());
+ sb.append("]"); //$NON-NLS-1$
+ Object cookie = info.getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ sb.append(" "); //$NON-NLS-1$
+ UiViewElementNode node = (UiViewElementNode) cookie;
+ sb.append("<"); //$NON-NLS-1$
+ sb.append(node.getDescriptor().getXmlName());
+ sb.append(">"); //$NON-NLS-1$
+
+ String id = node.getAttributeValue(ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ sb.append(" ");
+ sb.append(id);
+ }
+ } else if (cookie != null) {
+ sb.append(" " + cookie); //$NON-NLS-1$
+ }
+ /* Display defaults?
+ if (info.getViewObject() != null) {
+ Map<String, String> defaults = session.getDefaultProperties(info.getCookie());
+ sb.append(" - defaults: "); //$NON-NLS-1$
+ sb.append(defaults);
+ sb.append('\n');
+ }
+ */
+
+ System.out.println(sb.toString());
+
+ for (ViewInfo child : info.getChildren()) {
+ dump(session, child, depth + 1);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java
new file mode 100644
index 000000000..388907a46
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.AUTO_URI;
+import static com.android.SdkConstants.CLASS_FRAGMENT;
+import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
+import static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.URI_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IValidator;
+import com.android.ide.common.api.IViewMetadata;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
+import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.ui.MarginChooser;
+import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.ui.IJavaElementSearchConstants;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
+import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
+import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Implementation of {@link IClientRulesEngine}. This provides {@link IViewRule} clients
+ * with a few methods they can use to access functionality from this {@link RulesEngine}.
+ */
+class ClientRulesEngine implements IClientRulesEngine {
+ /** The return code from the dialog for the user choosing "Clear" */
+ public static final int CLEAR_RETURN_CODE = -5;
+ /** The dialog button ID for the user choosing "Clear" */
+ private static final int CLEAR_BUTTON_ID = CLEAR_RETURN_CODE;
+
+ private final RulesEngine mRulesEngine;
+ private final String mFqcn;
+
+ public ClientRulesEngine(RulesEngine rulesEngine, String fqcn) {
+ mRulesEngine = rulesEngine;
+ mFqcn = fqcn;
+ }
+
+ @Override
+ public @NonNull String getFqcn() {
+ return mFqcn;
+ }
+
+ @Override
+ public void debugPrintf(@NonNull String msg, Object... params) {
+ AdtPlugin.printToConsole(
+ mFqcn == null ? "<unknown>" : mFqcn,
+ String.format(msg, params)
+ );
+ }
+
+ @Override
+ public IViewRule loadRule(@NonNull String fqcn) {
+ return mRulesEngine.loadRule(fqcn, fqcn);
+ }
+
+ @Override
+ public void displayAlert(@NonNull String message) {
+ MessageDialog.openInformation(
+ AdtPlugin.getShell(),
+ mFqcn, // title
+ message);
+ }
+
+ @Override
+ public boolean rename(INode node) {
+ GraphicalEditorPart editor = mRulesEngine.getEditor();
+ SelectionManager manager = editor.getCanvasControl().getSelectionManager();
+ RenameResult result = manager.performRename(node, null);
+
+ return !result.isCanceled() && !result.isUnavailable();
+ }
+
+ @Override
+ public String displayInput(@NonNull String message, @Nullable String value,
+ final @Nullable IValidator filter) {
+ IInputValidator validator = null;
+ if (filter != null) {
+ validator = new IInputValidator() {
+ @Override
+ public String isValid(String newText) {
+ // IValidator has the same interface as SWT's IInputValidator
+ try {
+ return filter.validate(newText);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Custom validator failed: %s", e.toString());
+ return ""; //$NON-NLS-1$
+ }
+ }
+ };
+ }
+
+ InputDialog d = new InputDialog(
+ AdtPlugin.getShell(),
+ mFqcn, // title
+ message,
+ value == null ? "" : value, //$NON-NLS-1$
+ validator) {
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ createButton(parent, CLEAR_BUTTON_ID, "Clear", false /*defaultButton*/);
+ super.createButtonsForButtonBar(parent);
+ }
+
+ @Override
+ protected void buttonPressed(int buttonId) {
+ super.buttonPressed(buttonId);
+
+ if (buttonId == CLEAR_BUTTON_ID) {
+ assert CLEAR_RETURN_CODE != Window.OK && CLEAR_RETURN_CODE != Window.CANCEL;
+ setReturnCode(CLEAR_RETURN_CODE);
+ close();
+ }
+ }
+ };
+ int result = d.open();
+ if (result == ResourceChooser.CLEAR_RETURN_CODE) {
+ return "";
+ } else if (result == Window.OK) {
+ return d.getValue();
+ }
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Object getViewObject(@NonNull INode node) {
+ ViewHierarchy views = mRulesEngine.getEditor().getCanvasControl().getViewHierarchy();
+ CanvasViewInfo vi = views.findViewInfoFor(node);
+ if (vi != null) {
+ return vi.getViewObject();
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull IViewMetadata getMetadata(final @NonNull String fqcn) {
+ return new IViewMetadata() {
+ @Override
+ public @NonNull String getDisplayName() {
+ // This also works when there is no "."
+ return fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ }
+
+ @Override
+ public @NonNull FillPreference getFillPreference() {
+ return ViewMetadataRepository.get().getFillPreference(fqcn);
+ }
+
+ @Override
+ public @NonNull Margins getInsets() {
+ return mRulesEngine.getEditor().getCanvasControl().getInsets(fqcn);
+ }
+
+ @Override
+ public @NonNull List<String> getTopAttributes() {
+ return ViewMetadataRepository.get().getTopAttributes(fqcn);
+ }
+ };
+ }
+
+ @Override
+ public int getMinApiLevel() {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mRulesEngine.getEditor().getProject());
+ if (target != null) {
+ return target.getVersion().getApiLevel();
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public IValidator getResourceValidator(
+ @NonNull final String resourceTypeName, final boolean uniqueInProject,
+ final boolean uniqueInLayout, final boolean exists, final String... allowed) {
+ return new IValidator() {
+ private ResourceNameValidator mValidator;
+
+ @Override
+ public String validate(@NonNull String text) {
+ if (mValidator == null) {
+ ResourceType type = ResourceType.getEnum(resourceTypeName);
+ if (uniqueInLayout) {
+ assert !uniqueInProject;
+ assert !exists;
+ Set<String> existing = new HashSet<String>();
+ Document doc = mRulesEngine.getEditor().getModel().getXmlDocument();
+ if (doc != null) {
+ addIds(doc, existing);
+ }
+ for (String s : allowed) {
+ existing.remove(s);
+ }
+ mValidator = ResourceNameValidator.create(false, existing, type);
+ } else {
+ assert allowed.length == 0;
+ IProject project = mRulesEngine.getEditor().getProject();
+ mValidator = ResourceNameValidator.create(false, project, type);
+ if (uniqueInProject) {
+ mValidator.unique();
+ }
+ }
+ if (exists) {
+ mValidator.exist();
+ }
+ }
+
+ return mValidator.isValid(text);
+ }
+ };
+ }
+
+ /** Find declared ids under the given DOM node */
+ private static void addIds(Node node, Set<String> ids) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id != null && id.startsWith(NEW_ID_PREFIX)) {
+ ids.add(BaseViewRule.stripIdPrefix(id));
+ }
+ }
+
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ addIds(child, ids);
+ }
+ }
+
+ @Override
+ public String displayReferenceInput(@Nullable String currentValue) {
+ GraphicalEditorPart graphicalEditor = mRulesEngine.getEditor();
+ LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate();
+ IProject project = delegate.getEditor().getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceRepository projectRepository =
+ ResourceManager.getInstance().getProjectResources(project);
+ Shell shell = AdtPlugin.getShell();
+ if (shell == null) {
+ return null;
+ }
+ ReferenceChooserDialog dlg = new ReferenceChooserDialog(
+ project,
+ projectRepository,
+ shell);
+ dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor));
+
+ dlg.setCurrentResource(currentValue);
+
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String displayResourceInput(@NonNull String resourceTypeName,
+ @Nullable String currentValue) {
+ return displayResourceInput(resourceTypeName, currentValue, null);
+ }
+
+ private String displayResourceInput(String resourceTypeName, String currentValue,
+ IInputValidator validator) {
+ ResourceType type = ResourceType.getEnum(resourceTypeName);
+ GraphicalEditorPart graphicalEditor = mRulesEngine.getEditor();
+ return ResourceChooser.chooseResource(graphicalEditor, type, currentValue, validator);
+ }
+
+ @Override
+ public String[] displayMarginInput(@Nullable String all, @Nullable String left,
+ @Nullable String right, @Nullable String top, @Nullable String bottom) {
+ GraphicalEditorPart editor = mRulesEngine.getEditor();
+ IProject project = editor.getProject();
+ if (project != null) {
+ Shell shell = AdtPlugin.getShell();
+ if (shell == null) {
+ return null;
+ }
+ AndroidTargetData data = editor.getEditorDelegate().getEditor().getTargetData();
+ MarginChooser dialog = new MarginChooser(shell, editor, data, all, left, right,
+ top, bottom);
+ if (dialog.open() == Window.OK) {
+ return dialog.getMargins();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String displayIncludeSourceInput() {
+ AndroidXmlEditor editor = mRulesEngine.getEditor().getEditorDelegate().getEditor();
+ IInputValidator validator = CyclicDependencyValidator.create(editor.getInputFile());
+ return displayResourceInput(ResourceType.LAYOUT.getName(), null, validator);
+ }
+
+ @Override
+ public void select(final @NonNull Collection<INode> nodes) {
+ LayoutCanvas layoutCanvas = mRulesEngine.getEditor().getCanvasControl();
+ final SelectionManager selectionManager = layoutCanvas.getSelectionManager();
+ selectionManager.select(nodes);
+ // ALSO run an async select since immediately after nodes are created they
+ // may not be selectable. We can't ONLY run an async exec since
+ // code may depend on operating on the selection.
+ layoutCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ selectionManager.select(nodes);
+ }
+ });
+ }
+
+ @Override
+ public String displayFragmentSourceInput() {
+ try {
+ // Compute a search scope: We need to merge all the subclasses
+ // android.app.Fragment and android.support.v4.app.Fragment
+ IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
+ IProject project = mRulesEngine.getProject();
+ final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ IType oldFragmentType = javaProject.findType(CLASS_V4_FRAGMENT);
+
+ // First check to make sure fragments are available, and if not,
+ // warn the user.
+ IAndroidTarget target = Sdk.getCurrent().getTarget(project);
+ // No, this should be using the min SDK instead!
+ if (target.getVersion().getApiLevel() < 11 && oldFragmentType == null) {
+ // Compatibility library must be present
+ MessageDialog dialog =
+ new MessageDialog(
+ Display.getCurrent().getActiveShell(),
+ "Fragment Warning",
+ null,
+ "Fragments require API level 11 or higher, or a compatibility "
+ + "library for older versions.\n\n"
+ + " Do you want to install the compatibility library?",
+ MessageDialog.QUESTION,
+ new String[] { "Install", "Cancel" },
+ 1 /* default button: Cancel */);
+ int answer = dialog.open();
+ if (answer == 0) {
+ if (!AddSupportJarAction.install(project)) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ // Look up sub-types of each (new fragment class and compatibility fragment
+ // class, if any) and merge the two arrays - then create a scope from these
+ // elements.
+ IType[] fragmentTypes = new IType[0];
+ IType[] oldFragmentTypes = new IType[0];
+ if (oldFragmentType != null) {
+ ITypeHierarchy hierarchy =
+ oldFragmentType.newTypeHierarchy(new NullProgressMonitor());
+ oldFragmentTypes = hierarchy.getAllSubtypes(oldFragmentType);
+ }
+ IType fragmentType = javaProject.findType(CLASS_FRAGMENT);
+ if (fragmentType != null) {
+ ITypeHierarchy hierarchy =
+ fragmentType.newTypeHierarchy(new NullProgressMonitor());
+ fragmentTypes = hierarchy.getAllSubtypes(fragmentType);
+ }
+ IType[] subTypes = new IType[fragmentTypes.length + oldFragmentTypes.length];
+ System.arraycopy(fragmentTypes, 0, subTypes, 0, fragmentTypes.length);
+ System.arraycopy(oldFragmentTypes, 0, subTypes, fragmentTypes.length,
+ oldFragmentTypes.length);
+ scope = SearchEngine.createJavaSearchScope(subTypes, IJavaSearchScope.SOURCES);
+ }
+
+ Shell parent = AdtPlugin.getShell();
+ final AtomicReference<String> returnValue =
+ new AtomicReference<String>();
+ final AtomicReference<SelectionDialog> dialogHolder =
+ new AtomicReference<SelectionDialog>();
+ final SelectionDialog dialog = JavaUI.createTypeDialog(
+ parent,
+ new ProgressMonitorDialog(parent),
+ scope,
+ IJavaElementSearchConstants.CONSIDER_CLASSES, false,
+ // Use ? as a default filter to fill dialog with matches
+ "?", //$NON-NLS-1$
+ new TypeSelectionExtension() {
+ @Override
+ public Control createContentArea(Composite parentComposite) {
+ Composite composite = new Composite(parentComposite, SWT.NONE);
+ composite.setLayout(new GridLayout(1, false));
+ Button button = new Button(composite, SWT.PUSH);
+ button.setText("Create New...");
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String fqcn = createNewFragmentClass(javaProject);
+ if (fqcn != null) {
+ returnValue.set(fqcn);
+ dialogHolder.get().close();
+ }
+ }
+ });
+ return composite;
+ }
+
+ @Override
+ public ITypeInfoFilterExtension getFilterExtension() {
+ return new ITypeInfoFilterExtension() {
+ @Override
+ public boolean select(ITypeInfoRequestor typeInfoRequestor) {
+ int modifiers = typeInfoRequestor.getModifiers();
+ if (!Flags.isPublic(modifiers)
+ || Flags.isInterface(modifiers)
+ || Flags.isEnum(modifiers)
+ || Flags.isAbstract(modifiers)) {
+ return false;
+ }
+ return true;
+ }
+ };
+ }
+ });
+ dialogHolder.set(dialog);
+
+ dialog.setTitle("Choose Fragment Class");
+ dialog.setMessage("Select a Fragment class (? = any character, * = any string):");
+ if (dialog.open() == IDialogConstants.CANCEL_ID) {
+ return null;
+ }
+ if (returnValue.get() != null) {
+ return returnValue.get();
+ }
+
+ Object[] types = dialog.getResult();
+ if (types != null && types.length > 0) {
+ return ((IType) types[0]).getFullyQualifiedName();
+ }
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return null;
+ }
+
+ @Override
+ public String displayCustomViewClassInput() {
+ try {
+ IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
+ IProject project = mRulesEngine.getProject();
+ final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ // Look up sub-types of each (new fragment class and compatibility fragment
+ // class, if any) and merge the two arrays - then create a scope from these
+ // elements.
+ IType[] viewTypes = new IType[0];
+ IType fragmentType = javaProject.findType(CLASS_VIEW);
+ if (fragmentType != null) {
+ ITypeHierarchy hierarchy =
+ fragmentType.newTypeHierarchy(new NullProgressMonitor());
+ viewTypes = hierarchy.getAllSubtypes(fragmentType);
+ }
+ scope = SearchEngine.createJavaSearchScope(viewTypes, IJavaSearchScope.SOURCES);
+ }
+
+ Shell parent = AdtPlugin.getShell();
+ final AtomicReference<String> returnValue =
+ new AtomicReference<String>();
+ final AtomicReference<SelectionDialog> dialogHolder =
+ new AtomicReference<SelectionDialog>();
+ final SelectionDialog dialog = JavaUI.createTypeDialog(
+ parent,
+ new ProgressMonitorDialog(parent),
+ scope,
+ IJavaElementSearchConstants.CONSIDER_CLASSES, false,
+ // Use ? as a default filter to fill dialog with matches
+ "?", //$NON-NLS-1$
+ new TypeSelectionExtension() {
+ @Override
+ public Control createContentArea(Composite parentComposite) {
+ Composite composite = new Composite(parentComposite, SWT.NONE);
+ composite.setLayout(new GridLayout(1, false));
+ Button button = new Button(composite, SWT.PUSH);
+ button.setText("Create New...");
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String fqcn = createNewCustomViewClass(javaProject);
+ if (fqcn != null) {
+ returnValue.set(fqcn);
+ dialogHolder.get().close();
+ }
+ }
+ });
+ return composite;
+ }
+
+ @Override
+ public ITypeInfoFilterExtension getFilterExtension() {
+ return new ITypeInfoFilterExtension() {
+ @Override
+ public boolean select(ITypeInfoRequestor typeInfoRequestor) {
+ int modifiers = typeInfoRequestor.getModifiers();
+ if (!Flags.isPublic(modifiers)
+ || Flags.isInterface(modifiers)
+ || Flags.isEnum(modifiers)
+ || Flags.isAbstract(modifiers)) {
+ return false;
+ }
+ return true;
+ }
+ };
+ }
+ });
+ dialogHolder.set(dialog);
+
+ dialog.setTitle("Choose Custom View Class");
+ dialog.setMessage("Select a Custom View class (? = any character, * = any string):");
+ if (dialog.open() == IDialogConstants.CANCEL_ID) {
+ return null;
+ }
+ if (returnValue.get() != null) {
+ return returnValue.get();
+ }
+
+ Object[] types = dialog.getResult();
+ if (types != null && types.length > 0) {
+ return ((IType) types[0]).getFullyQualifiedName();
+ }
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return null;
+ }
+
+ @Override
+ public void redraw() {
+ mRulesEngine.getEditor().getCanvasControl().redraw();
+ }
+
+ @Override
+ public void layout() {
+ mRulesEngine.getEditor().recomputeLayout();
+ }
+
+ @Override
+ public Map<INode, Rect> measureChildren(@NonNull INode parent,
+ @Nullable IClientRulesEngine.AttributeFilter filter) {
+ RenderService renderService = RenderService.create(mRulesEngine.getEditor());
+ Map<INode, Rect> map = renderService.measureChildren(parent, filter);
+ if (map == null) {
+ map = Collections.emptyMap();
+ }
+ return map;
+ }
+
+ @Override
+ public int pxToDp(int px) {
+ ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser();
+ float dpi = chooser.getConfiguration().getDensity().getDpiValue();
+ return (int) (px * 160 / dpi);
+ }
+
+ @Override
+ public int dpToPx(int dp) {
+ ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser();
+ float dpi = chooser.getConfiguration().getDensity().getDpiValue();
+ return (int) (dp * dpi / 160);
+ }
+
+ @Override
+ public int screenToLayout(int pixels) {
+ return (int) (pixels / mRulesEngine.getEditor().getCanvasControl().getScale());
+ }
+
+ private String createNewFragmentClass(IJavaProject javaProject) {
+ NewClassWizardPage page = new NewClassWizardPage();
+
+ IProject project = mRulesEngine.getProject();
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk == null) {
+ return null;
+ }
+ IAndroidTarget target = sdk.getTarget(project);
+ String superClass;
+ if (target == null || target.getVersion().getApiLevel() < 11) {
+ superClass = CLASS_V4_FRAGMENT;
+ } else {
+ superClass = CLASS_FRAGMENT;
+ }
+ page.setSuperClass(superClass, true /* canBeModified */);
+ IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject);
+ if (root != null) {
+ page.setPackageFragmentRoot(root, true /* canBeModified */);
+ }
+ ManifestInfo manifestInfo = ManifestInfo.get(project);
+ IPackageFragment pkg = manifestInfo.getPackageFragment();
+ if (pkg != null) {
+ page.setPackageFragment(pkg, true /* canBeModified */);
+ }
+ OpenNewClassWizardAction action = new OpenNewClassWizardAction();
+ action.setConfiguredWizardPage(page);
+ action.run();
+ IType createdType = page.getCreatedType();
+ if (createdType != null) {
+ return createdType.getFullyQualifiedName();
+ } else {
+ return null;
+ }
+ }
+
+ private String createNewCustomViewClass(IJavaProject javaProject) {
+ NewClassWizardPage page = new NewClassWizardPage();
+
+ IProject project = mRulesEngine.getProject();
+ String superClass = CLASS_VIEW;
+ page.setSuperClass(superClass, true /* canBeModified */);
+ IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject);
+ if (root != null) {
+ page.setPackageFragmentRoot(root, true /* canBeModified */);
+ }
+ ManifestInfo manifestInfo = ManifestInfo.get(project);
+ IPackageFragment pkg = manifestInfo.getPackageFragment();
+ if (pkg != null) {
+ page.setPackageFragment(pkg, true /* canBeModified */);
+ }
+ OpenNewClassWizardAction action = new OpenNewClassWizardAction();
+ action.setConfiguredWizardPage(page);
+ action.run();
+ IType createdType = page.getCreatedType();
+ if (createdType != null) {
+ return createdType.getFullyQualifiedName();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public @NonNull String getUniqueId(@NonNull String fqcn) {
+ UiDocumentNode root = mRulesEngine.getEditor().getModel();
+ String prefix = fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1);
+ return DescriptorsUtils.getFreeWidgetId(root, prefix);
+ }
+
+ @Override
+ public @NonNull String getAppNameSpace() {
+ IProject project = mRulesEngine.getEditor().getProject();
+
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null && projectState.isLibrary()) {
+ return AUTO_URI;
+ }
+
+ ManifestInfo info = ManifestInfo.get(project);
+ return URI_PREFIX + info.getPackage();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java
new file mode 100644
index 000000000..b0b9971ba
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import com.android.ide.common.api.INode;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * An object that can create {@link INode} proxies.
+ * This also keeps references to objects already created and tries to reuse them.
+ */
+public class NodeFactory {
+
+ private final Map<UiViewElementNode, NodeProxy> mNodeMap =
+ new WeakHashMap<UiViewElementNode, NodeProxy>();
+ private LayoutCanvas mCanvas;
+
+ public NodeFactory(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * Returns an {@link INode} proxy based on the view key of the given
+ * {@link CanvasViewInfo}. The bounds of the node are set to the canvas view bounds.
+ */
+ public NodeProxy create(CanvasViewInfo canvasViewInfo) {
+ return create(canvasViewInfo.getUiViewNode(), canvasViewInfo.getAbsRect());
+ }
+
+ /**
+ * Returns an {@link INode} proxy based on a given {@link UiViewElementNode} that
+ * is not yet part of the canvas, typically those created by layout rules
+ * when generating new XML.
+ */
+ public NodeProxy create(UiViewElementNode uiNode) {
+ return create(uiNode, null /*bounds*/);
+ }
+
+ public void clear() {
+ mNodeMap.clear();
+ }
+
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ //----
+
+ private NodeProxy create(UiViewElementNode uiNode, Rectangle bounds) {
+ NodeProxy proxy = mNodeMap.get(uiNode);
+
+ if (proxy == null) {
+ // Create a new proxy if the key doesn't exist
+ proxy = new NodeProxy(uiNode, bounds, this);
+ mNodeMap.put(uiNode, proxy);
+
+ } else if (bounds != null && !SwtUtils.equals(proxy.getBounds(), bounds)) {
+ // Update the bounds if necessary
+ proxy.setBounds(bounds);
+ }
+
+ return proxy;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java
new file mode 100644
index 000000000..19d5e16b0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INodeHandler;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ */
+public class NodeProxy implements INode {
+ private static final Margins NO_MARGINS = new Margins(0, 0, 0, 0);
+ private final UiViewElementNode mNode;
+ private final Rect mBounds;
+ private final NodeFactory mFactory;
+ /** Map from URI to Map(key=>value) (where no namespace uses "" as a key) */
+ private Map<String, Map<String, String>> mPendingAttributes;
+
+ /**
+ * Creates a new {@link INode} that wraps an {@link UiViewElementNode} that is
+ * actually valid in the current UI/XML model. The view may not be part of the canvas
+ * yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.)
+ * <p/>
+ * This method is package protected. To create a node, please use {@link NodeFactory} instead.
+ *
+ * @param uiNode The node to wrap.
+ * @param bounds The bounds of a the view in the canvas. Must be either: <br/>
+ * - a valid rect for a view that is actually in the canvas <br/>
+ * - <b>*or*</b> null (or an invalid rect) for a view that has just been added dynamically
+ * to the model. We never store a null bounds rectangle in the node, a null rectangle
+ * will be converted to an invalid rectangle.
+ * @param factory A {@link NodeFactory} to create unique children nodes.
+ */
+ /*package*/ NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory) {
+ mNode = uiNode;
+ mFactory = factory;
+ if (bounds == null) {
+ mBounds = new Rect();
+ } else {
+ mBounds = SwtUtils.toRect(bounds);
+ }
+ }
+
+ @Override
+ public @NonNull Rect getBounds() {
+ return mBounds;
+ }
+
+ @Override
+ public @NonNull Margins getMargins() {
+ ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy();
+ CanvasViewInfo view = viewHierarchy.findViewInfoFor(this);
+ if (view != null) {
+ Margins margins = view.getMargins();
+ if (margins != null) {
+ return margins;
+ }
+ }
+
+ return NO_MARGINS;
+ }
+
+
+ @Override
+ public int getBaseline() {
+ ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy();
+ CanvasViewInfo view = viewHierarchy.findViewInfoFor(this);
+ if (view != null) {
+ return view.getBaseline();
+ }
+
+ return -1;
+ }
+
+ /**
+ * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid.
+ * This is a package-protected method, only the {@link NodeFactory} uses this method.
+ */
+ /*package*/ void setBounds(Rectangle bounds) {
+ SwtUtils.set(mBounds, bounds);
+ }
+
+ /**
+ * Returns the {@link UiViewElementNode} corresponding to this
+ * {@link NodeProxy}.
+ *
+ * @return The {@link UiViewElementNode} corresponding to this
+ * {@link NodeProxy}
+ */
+ public UiViewElementNode getNode() {
+ return mNode;
+ }
+
+ @Override
+ public @NonNull String getFqcn() {
+ if (mNode != null) {
+ ElementDescriptor desc = mNode.getDescriptor();
+ if (desc instanceof ViewElementDescriptor) {
+ return ((ViewElementDescriptor) desc).getFullClassName();
+ }
+ }
+
+ return "";
+ }
+
+
+ // ---- Hierarchy handling ----
+
+
+ @Override
+ public INode getRoot() {
+ if (mNode != null) {
+ UiElementNode p = mNode.getUiRoot();
+ // The node root should be a document. Instead what we really mean to
+ // return is the top level view element.
+ if (p instanceof UiDocumentNode) {
+ List<UiElementNode> children = p.getUiChildren();
+ if (children.size() > 0) {
+ p = children.get(0);
+ }
+ }
+
+ // Cope with a badly structured XML layout
+ while (p != null && !(p instanceof UiViewElementNode)) {
+ p = p.getUiNextSibling();
+ }
+
+ if (p == mNode) {
+ return this;
+ }
+ if (p instanceof UiViewElementNode) {
+ return mFactory.create((UiViewElementNode) p);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public INode getParent() {
+ if (mNode != null) {
+ UiElementNode p = mNode.getUiParent();
+ if (p instanceof UiViewElementNode) {
+ return mFactory.create((UiViewElementNode) p);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull INode[] getChildren() {
+ if (mNode != null) {
+ List<UiElementNode> uiChildren = mNode.getUiChildren();
+ List<INode> nodes = new ArrayList<INode>(uiChildren.size());
+ for (UiElementNode uiChild : uiChildren) {
+ if (uiChild instanceof UiViewElementNode) {
+ nodes.add(mFactory.create((UiViewElementNode) uiChild));
+ }
+ }
+
+ return nodes.toArray(new INode[nodes.size()]);
+ }
+
+ return new INode[0];
+ }
+
+
+ // ---- XML Editing ---
+
+ @Override
+ public void editXml(@NonNull String undoName, final @NonNull INodeHandler c) {
+ final AndroidXmlEditor editor = mNode.getEditor();
+
+ if (editor != null) {
+ // Create an undo edit XML wrapper, which takes a runnable
+ editor.wrapUndoEditXmlModel(
+ undoName,
+ new Runnable() {
+ @Override
+ public void run() {
+ // Here editor.isEditXmlModelPending returns true and it
+ // is safe to edit the model using any method from INode.
+
+ // Finally execute the closure that will act on the XML
+ c.handle(NodeProxy.this);
+ applyPendingChanges();
+ }
+ });
+ }
+ }
+
+ private void checkEditOK() {
+ final AndroidXmlEditor editor = mNode.getEditor();
+ if (!editor.isEditXmlModelPending()) {
+ throw new RuntimeException("Error: XML edit call without using INode.editXml!");
+ }
+ }
+
+ @Override
+ public @NonNull INode appendChild(@NonNull String viewFqcn) {
+ return insertOrAppend(viewFqcn, -1);
+ }
+
+ @Override
+ public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) {
+ return insertOrAppend(viewFqcn, index);
+ }
+
+ @Override
+ public void removeChild(@NonNull INode node) {
+ checkEditOK();
+
+ ((NodeProxy) node).mNode.deleteXmlNode();
+ }
+
+ private INode insertOrAppend(String viewFqcn, int index) {
+ checkEditOK();
+
+ AndroidXmlEditor editor = mNode.getEditor();
+ if (editor != null) {
+ // Possibly replace the tag with a compatibility version if the
+ // minimum SDK requires it
+ IProject project = editor.getProject();
+ if (project != null) {
+ viewFqcn = SupportLibraryHelper.getTagFor(project, viewFqcn);
+ }
+ }
+
+ // Find the descriptor for this FQCN
+ ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn);
+ if (vd == null) {
+ warnPrintf("Can't create a new %s element", viewFqcn);
+ return null;
+ }
+
+ final UiElementNode uiNew;
+ if (index == -1) {
+ // Append at the end.
+ uiNew = mNode.appendNewUiChild(vd);
+ } else {
+ // Insert at the requested position or at the end.
+ int n = mNode.getUiChildren().size();
+ if (index < 0 || index >= n) {
+ uiNew = mNode.appendNewUiChild(vd);
+ } else {
+ uiNew = mNode.insertNewUiChild(index, vd);
+ }
+ }
+
+ // Set default attributes -- but only for new widgets (not when moving or copying)
+ RulesEngine engine = null;
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
+ if (delegate != null) {
+ engine = delegate.getRulesEngine();
+ }
+ if (engine == null || engine.getInsertType().isCreate()) {
+ // TODO: This should probably use IViewRule#getDefaultAttributes() at some point
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
+ }
+
+ Node xmlNode = uiNew.createXmlNode();
+
+ if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) {
+ // Both things are not supposed to happen. When they do, we're in big trouble.
+ // We don't really know how to revert the state at this point and the UI model is
+ // now out of sync with the XML model.
+ // Panic ensues.
+ // The best bet is to abort now. The edit wrapper will release the edit and the
+ // XML/UI should get reloaded properly (with a likely invalid XML.)
+ warnPrintf("Failed to create a new %s element", viewFqcn);
+ throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$
+ }
+
+ UiViewElementNode uiNewView = (UiViewElementNode) uiNew;
+ NodeProxy newNode = mFactory.create(uiNewView);
+
+ if (engine != null) {
+ engine.callCreateHooks(editor, this, newNode, null);
+ }
+
+ return newNode;
+ }
+
+ @Override
+ public boolean setAttribute(
+ @Nullable String uri,
+ @NonNull String name,
+ @Nullable String value) {
+ checkEditOK();
+ UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */);
+
+ if (uri == null) {
+ uri = ""; //$NON-NLS-1$
+ }
+
+ Map<String, String> map = null;
+ if (mPendingAttributes == null) {
+ // Small initial size: we don't expect many different namespaces
+ mPendingAttributes = new HashMap<String, Map<String, String>>(3);
+ } else {
+ map = mPendingAttributes.get(uri);
+ }
+ if (map == null) {
+ map = new HashMap<String, String>();
+ mPendingAttributes.put(uri, map);
+ }
+ map.put(name, value);
+
+ return attr != null;
+ }
+
+ @Override
+ public String getStringAttr(@Nullable String uri, @NonNull String attrName) {
+ UiElementNode uiNode = mNode;
+
+ if (attrName == null) {
+ return null;
+ }
+
+ if (mPendingAttributes != null) {
+ Map<String, String> map = mPendingAttributes.get(uri == null ? "" : uri); //$NON-NLS-1$
+ if (map != null) {
+ String value = map.get(attrName);
+ if (value != null) {
+ return value;
+ }
+ }
+ }
+
+ if (uiNode.getXmlNode() != null) {
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode != null) {
+ NamedNodeMap nodeAttributes = xmlNode.getAttributes();
+ if (nodeAttributes != null) {
+ Node attr = nodeAttributes.getNamedItemNS(uri, attrName);
+ if (attr != null) {
+ return attr.getNodeValue();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) {
+ UiElementNode uiNode = mNode;
+
+ if (attrName == null) {
+ return null;
+ }
+
+ for (AttributeDescriptor desc : uiNode.getAttributeDescriptors()) {
+ String dUri = desc.getNamespaceUri();
+ String dName = desc.getXmlLocalName();
+ if ((uri == null && dUri == null) || (uri != null && uri.equals(dUri))) {
+ if (attrName.equals(dName)) {
+ return desc.getAttributeInfo();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull IAttributeInfo[] getDeclaredAttributes() {
+
+ AttributeDescriptor[] descs = mNode.getAttributeDescriptors();
+ int n = descs.length;
+ IAttributeInfo[] infos = new AttributeInfo[n];
+
+ for (int i = 0; i < n; i++) {
+ infos[i] = descs[i].getAttributeInfo();
+ }
+
+ return infos;
+ }
+
+ @Override
+ public @NonNull List<String> getAttributeSources() {
+ ElementDescriptor descriptor = mNode.getDescriptor();
+ if (descriptor instanceof ViewElementDescriptor) {
+ return ((ViewElementDescriptor) descriptor).getAttributeSources();
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public @NonNull IAttribute[] getLiveAttributes() {
+ UiElementNode uiNode = mNode;
+
+ if (uiNode.getXmlNode() != null) {
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode != null) {
+ NamedNodeMap nodeAttributes = xmlNode.getAttributes();
+ if (nodeAttributes != null) {
+
+ int n = nodeAttributes.getLength();
+ IAttribute[] result = new IAttribute[n];
+ for (int i = 0; i < n; i++) {
+ Node attr = nodeAttributes.item(i);
+ String uri = attr.getNamespaceURI();
+ String name = attr.getLocalName();
+ String value = attr.getNodeValue();
+
+ result[i] = new SimpleAttribute(uri, name, value);
+ }
+ return result;
+ }
+ }
+ }
+
+ return new IAttribute[0];
+
+ }
+
+ @Override
+ public String toString() {
+ return "NodeProxy [node=" + mNode + ", bounds=" + mBounds + "]";
+ }
+
+ // --- internal helpers ---
+
+ /**
+ * Helper methods that returns a {@link ViewElementDescriptor} for the requested FQCN.
+ * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info
+ * (which shouldn't really happen since at this point the SDK should be fully loaded and
+ * isn't reloading, or we wouldn't be here editing XML for a layout rule.)
+ */
+ private ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(mNode.getEditor());
+ if (delegate != null) {
+ return delegate.getFqcnViewDescriptor(fqcn);
+ }
+
+ return null;
+ }
+
+ private void warnPrintf(String msg, Object...params) {
+ AdtPlugin.printToConsole(
+ mNode == null ? "" : mNode.getDescriptor().getXmlLocalName(),
+ String.format(msg, params)
+ );
+ }
+
+ /**
+ * If there are any pending changes in these nodes, apply them now
+ *
+ * @return true if any modifications were made
+ */
+ public boolean applyPendingChanges() {
+ boolean modified = false;
+
+ // Flush all pending attributes
+ if (mPendingAttributes != null) {
+ mNode.commitDirtyAttributesToXml();
+ modified = true;
+ mPendingAttributes = null;
+
+ }
+ for (INode child : getChildren()) {
+ modified |= ((NodeProxy) child).applyPendingChanges();
+ }
+
+ return modified;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java
new file mode 100644
index 000000000..884cb077a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement;
+
+import org.eclipse.swt.graphics.Image;
+import org.w3c.dom.Element;
+
+/**
+ * Special version of {@link ViewElementDescriptor} which is initialized by the palette
+ * with specific metadata for how to instantiate particular variations of an existing
+ * {@link ViewElementDescriptor} with initial values.
+ */
+public class PaletteMetadataDescriptor extends ViewElementDescriptor {
+ private String mInitString;
+ private String mIconName;
+
+ public PaletteMetadataDescriptor(ViewElementDescriptor descriptor, String displayName,
+ String initString, String iconName) {
+ super(descriptor.getXmlName(),
+ displayName,
+ descriptor.getFullClassName(),
+ descriptor.getTooltip(),
+ descriptor.getSdkUrl(),
+ descriptor.getAttributes(),
+ descriptor.getLayoutAttributes(),
+ descriptor.getChildren(), descriptor.getMandatory() == Mandatory.MANDATORY);
+ mInitString = initString;
+ mIconName = iconName;
+ setSuperClass(descriptor.getSuperClassDesc());
+ }
+
+ /**
+ * Returns a String which contains a comma separated list of name=value tokens,
+ * where the name can start with "android:" to indicate a property in the android namespace,
+ * or no prefix for plain attributes.
+ *
+ * @return the initialization string, which can be empty but never null
+ */
+ public String getInitializedAttributes() {
+ return mInitString != null ? mInitString : ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public Image getGenericIcon() {
+ if (mIconName != null) {
+ IconFactory factory = IconFactory.getInstance();
+ Image icon = factory.getIcon(mIconName);
+ if (icon != null) {
+ return icon;
+ }
+ }
+
+ return super.getGenericIcon();
+ }
+
+ /**
+ * Initializes a new {@link SimpleElement} with the palette initialization
+ * configuration
+ *
+ * @param element the new element to initialize
+ */
+ public void initializeNew(SimpleElement element) {
+ initializeNew(element, null);
+ }
+
+ /**
+ * Initializes a new {@link Element} with the palette initialization configuration
+ *
+ * @param element the new element to initialize
+ */
+ public void initializeNew(Element element) {
+ initializeNew(null, element);
+ }
+
+ private void initializeNew(SimpleElement simpleElement, Element domElement) {
+ String initializedAttributes = mInitString;
+ if (initializedAttributes != null && initializedAttributes.length() > 0) {
+ for (String s : initializedAttributes.split(",")) { //$NON-NLS-1$
+ String[] nameValue = s.split("="); //$NON-NLS-1$
+ String name = nameValue[0];
+ String value = nameValue[1];
+ String nameSpace = ""; //$NON-NLS-1$
+ if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
+ name = name.substring(ANDROID_NS_NAME_PREFIX.length());
+ nameSpace = ANDROID_URI;
+ }
+
+ if (simpleElement != null) {
+ SimpleAttribute attr = new SimpleAttribute(nameSpace, name, value);
+ simpleElement.addAttribute(attr);
+ }
+
+ if (domElement != null) {
+ domElement.setAttributeNS(nameSpace, name, value);
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java
new file mode 100644
index 000000000..4f49a7545
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link RuleLoader} is responsible for loading (and unloading)
+ * {@link IViewRule} classes. There is typically one {@link RuleLoader}
+ * per project.
+ */
+public class RuleLoader {
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link RuleLoader} for this project
+ */
+ private final static QualifiedName RULE_LOADER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "ruleloader"); //$NON-NLS-1$
+
+ private final IProject mProject;
+ private ClassLoader mUserClassLoader;
+ private List<Pair<File, Long>> mUserJarTimeStamps;
+ private long mLastCheckTimeStamp;
+
+ /**
+ * Flag set when we've attempted to initialize the {@link #mUserClassLoader}
+ * already
+ */
+ private boolean mUserClassLoaderInited;
+
+ /**
+ * Returns the {@link RuleLoader} for the given project
+ *
+ * @param project the project the loader is associated with
+ * @return an {@RuleLoader} for the given project,
+ * never null
+ */
+ public static RuleLoader get(IProject project) {
+ RuleLoader loader = null;
+ try {
+ loader = (RuleLoader) project.getSessionProperty(RULE_LOADER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+ if (loader == null) {
+ loader = new RuleLoader(project);
+ try {
+ project.setSessionProperty(RULE_LOADER, loader);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store RuleLoader");
+ }
+ }
+ return loader;
+ }
+
+ /** Do not call; use the {@link #get} factory method instead. */
+ private RuleLoader(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Find out whether the given project has 3rd party ViewRules, and if so
+ * return a ClassLoader which can locate them. If not, return null.
+ * @param project The project to load user rules from
+ * @return A class loader which can user view rules, or otherwise null
+ */
+ private ClassLoader computeUserClassLoader(IProject project) {
+ // Default place to locate layout rules. The user may also add to this
+ // path by defining a config property specifying
+ // additional .jar files to search via a the layoutrules.jars property.
+ ProjectState state = Sdk.getProjectState(project);
+ ProjectProperties projectProperties = state.getProperties();
+
+ // Ensure we have the latest & greatest version of the properties.
+ // This allows users to reopen editors in a running Eclipse instance
+ // to get updated view rule jars
+ projectProperties.reload();
+
+ String path = projectProperties.getProperty(
+ ProjectProperties.PROPERTY_RULES_PATH);
+
+ if (path != null && path.length() > 0) {
+
+ mUserJarTimeStamps = new ArrayList<Pair<File, Long>>();
+ mLastCheckTimeStamp = System.currentTimeMillis();
+
+ List<URL> urls = new ArrayList<URL>();
+ String[] pathElements = path.split(File.pathSeparator);
+ for (String pathElement : pathElements) {
+ pathElement = pathElement.trim(); // Avoid problems with trailing whitespace etc
+ File pathFile = new File(pathElement);
+ if (!pathFile.isAbsolute()) {
+ pathFile = new File(project.getLocation().toFile(), pathElement);
+ }
+ // Directories and jar files are okay. Do we need to
+ // validate the files here as .jar files?
+ if (pathFile.isFile() || pathFile.isDirectory()) {
+ URL url;
+ try {
+ url = pathFile.toURI().toURL();
+ urls.add(url);
+
+ mUserJarTimeStamps.add(Pair.of(pathFile, pathFile.lastModified()));
+ } catch (MalformedURLException e) {
+ AdtPlugin.log(IStatus.WARNING,
+ "Invalid URL: %1$s", //$NON-NLS-1$
+ e.toString());
+ }
+ }
+ }
+
+ if (urls.size() > 0) {
+ return new URLClassLoader(urls.toArray(new URL[urls.size()]),
+ RulesEngine.class.getClassLoader());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the class loader to use for custom views, or null if no custom
+ * view rules are registered for the project. Note that this class loader
+ * can change over time (if the jar files are updated), so callers should be
+ * prepared to unload previous instances.
+ *
+ * @return a class loader to use for custom view rules, or null
+ */
+ public ClassLoader getClassLoader() {
+ if (mUserClassLoader == null) {
+ // Only attempt to load rule paths once.
+ // TODO: Check the timestamp on the project.properties file so we can dynamically
+ // pick up cases where the user edits the path
+ if (!mUserClassLoaderInited) {
+ mUserClassLoaderInited = true;
+ mUserClassLoader = computeUserClassLoader(mProject);
+ }
+ } else {
+ // Check the timestamp on the jar files in the custom view path to see if we
+ // need to reload the classes (but only do this at most every 3 seconds)
+ if (mUserJarTimeStamps != null) {
+ long time = System.currentTimeMillis();
+ if (time - mLastCheckTimeStamp > 3000) {
+ mLastCheckTimeStamp = time;
+ for (Pair<File, Long> pair : mUserJarTimeStamps) {
+ File file = pair.getFirst();
+ Long prevModified = pair.getSecond();
+ long modified = file.lastModified();
+ if (prevModified.longValue() != modified) {
+ mUserClassLoaderInited = true;
+ mUserJarTimeStamps = null;
+ mUserClassLoader = computeUserClassLoader(mProject);
+ }
+ }
+ }
+ }
+ }
+
+ return mUserClassLoader;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java
new file mode 100644
index 000000000..8f9923749
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java
@@ -0,0 +1,876 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.VIEW_MERGE;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.ViewRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The rule engine manages the layout rules and interacts with them.
+ * There's one {@link RulesEngine} instance per layout editor.
+ * Each instance has 2 sets of rules: the static ADT rules (shared across all instances)
+ * and the project specific rules (local to the current instance / layout editor).
+ */
+public class RulesEngine {
+ private final IProject mProject;
+ private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>();
+
+ /**
+ * The type of any upcoming node manipulations performed by the {@link IViewRule}s.
+ * When actions are performed in the tool (like a paste action, or a drag from palette,
+ * or a drag move within the canvas, etc), these are different types of inserts,
+ * and we don't want to have the rules track them closely (and pass them back to us
+ * in the {@link INode#insertChildAt} methods etc), so instead we track the state
+ * here on behalf of the currently executing rule.
+ */
+ private InsertType mInsertType = InsertType.CREATE;
+
+ /**
+ * Per-project loader for custom view rules
+ */
+ private RuleLoader mRuleLoader;
+ private ClassLoader mUserClassLoader;
+
+ /**
+ * The editor which owns this {@link RulesEngine}
+ */
+ private final GraphicalEditorPart mEditor;
+
+ /**
+ * Creates a new {@link RulesEngine} associated with the selected project.
+ * <p/>
+ * The rules engine will look in the project for a tools jar to load custom view rules.
+ *
+ * @param editor the editor which owns this {@link RulesEngine}
+ * @param project A non-null open project.
+ */
+ public RulesEngine(GraphicalEditorPart editor, IProject project) {
+ mProject = project;
+ mEditor = editor;
+
+ mRuleLoader = RuleLoader.get(project);
+ }
+
+ /**
+ * Returns the {@link IProject} on which the {@link RulesEngine} was created.
+ */
+ public IProject getProject() {
+ return mProject;
+ }
+
+ /**
+ * Returns the {@link GraphicalEditorPart} for which the {@link RulesEngine} was
+ * created.
+ *
+ * @return the associated editor
+ */
+ public GraphicalEditorPart getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Called by the owner of the {@link RulesEngine} when it is going to be disposed.
+ * This frees some resources, such as the project's folder monitor.
+ */
+ public void dispose() {
+ clearCache();
+ }
+
+ /**
+ * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element.
+ *
+ * @param element The view element to target. Can be null.
+ * @return Null if the rule failed, there's no rule or the rule does not want to override
+ * the display name. Otherwise, a string as returned by the rule.
+ */
+ public String callGetDisplayName(UiViewElementNode element) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(element);
+
+ if (rule != null) {
+ try {
+ return rule.getDisplayName();
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getDisplayName() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Invokes {@link IViewRule#addContextMenuActions(List, INode)} on the rule matching the specified element.
+ *
+ * @param selectedNode The node selected. Never null.
+ * @return Null if the rule failed, there's no rule or the rule does not provide
+ * any custom menu actions. Otherwise, a list of {@link RuleAction}.
+ */
+ @Nullable
+ public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(selectedNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.CREATE;
+ List<RuleAction> actions = new ArrayList<RuleAction>();
+ rule.addContextMenuActions(actions, selectedNode);
+ Collections.sort(actions);
+
+ return actions;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getContextMenu() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Calls the selected node to return its default action
+ *
+ * @param selectedNode the node to apply the action to
+ * @return the default action id
+ */
+ public String callGetDefaultActionId(@NonNull NodeProxy selectedNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(selectedNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.CREATE;
+ return rule.getDefaultActionId(selectedNode);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getDefaultAction() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule
+ * matching the specified element.
+ *
+ * @param actions The list of actions to add layout actions into
+ * @param parentNode The layout node
+ * @param children The selected children of the node, if any (used to
+ * initialize values of child layout controls, if applicable)
+ * @return Null if the rule failed, there's no rule or the rule does not
+ * provide any custom menu actions. Otherwise, a list of
+ * {@link RuleAction}.
+ */
+ public List<RuleAction> callAddLayoutActions(List<RuleAction> actions,
+ NodeProxy parentNode, List<NodeProxy> children ) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(parentNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.CREATE;
+ rule.addLayoutActions(actions, parentNode, children);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getContextMenu() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Invokes {@link IViewRule#getSelectionHint(INode, INode)}
+ * on the rule matching the specified element.
+ *
+ * @param parentNode The parent of the node selected. Never null.
+ * @param childNode The child node that was selected. Never null.
+ * @return a list of strings to be displayed, or null or empty to display nothing
+ */
+ public List<String> callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(parentNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.getSelectionHint(parentNode, childNode);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getSelectionHint() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode,
+ List<? extends INode> childNodes, Object view) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(parentNode.getNode());
+
+ if (rule != null) {
+ try {
+ rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes, view);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when the d'n'd starts dragging over the target node.
+ * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint.
+ * If not interested in drop, return false.
+ * Followed by a paint.
+ */
+ public DropFeedback callOnDropEnter(NodeProxy targetNode,
+ Object targetView, IDragElement[] elements) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.onDropEnter(targetNode, targetView, elements);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropEnter() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Called after onDropEnter.
+ * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same
+ * as input one).
+ */
+ public DropFeedback callOnDropMove(NodeProxy targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback,
+ Point where) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.onDropMove(targetNode, elements, feedback, where);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropMove() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Called when drop leaves the target without actually dropping
+ */
+ public void callOnDropLeave(NodeProxy targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ rule.onDropLeave(targetNode, elements, feedback);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropLeave() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when drop is released over the target to perform the actual drop.
+ */
+ public void callOnDropped(NodeProxy targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback,
+ Point where,
+ InsertType insertType) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = insertType;
+ rule.onDropped(targetNode, elements, feedback, where);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropped() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when a paint has been requested via DropFeedback.
+ */
+ public void callDropFeedbackPaint(IGraphics gc,
+ NodeProxy targetNode,
+ DropFeedback feedback) {
+ if (gc != null && feedback != null && feedback.painter != null) {
+ try {
+ feedback.painter.paint(gc, targetNode, feedback);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "DropFeedback.painter failed: %s",
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when pasting elements in an existing document on the selected target.
+ *
+ * @param targetNode The first node selected.
+ * @param targetView The view object for the target node, or null if not known
+ * @param pastedElements The elements being pasted.
+ * @return the parent node the paste was applied into
+ */
+ public NodeProxy callOnPaste(NodeProxy targetNode, Object targetView,
+ SimpleElement[] pastedElements) {
+
+ // Find a target which accepts children. If you for example select a button
+ // and attempt to paste, this will reselect the parent of the button as the paste
+ // target. (This is a loop rather than just checking the direct parent since
+ // we will soon ask each child whether they are *willing* to accept the new child.
+ // A ScrollView for example, which only accepts one child, might also say no
+ // and delegate to its parent in turn.
+ INode parent = targetNode;
+ while (parent instanceof NodeProxy) {
+ NodeProxy np = (NodeProxy) parent;
+ if (np.getNode() != null && np.getNode().getDescriptor() != null) {
+ ElementDescriptor descriptor = np.getNode().getDescriptor();
+ if (descriptor.hasChildren()) {
+ targetNode = np;
+ break;
+ }
+ }
+ parent = parent.getParent();
+ }
+
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.PASTE;
+ rule.onPaste(targetNode, targetView, pastedElements);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onPaste() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return targetNode;
+ }
+
+ // ---- Resize operations ----
+
+ public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds,
+ SegmentType horizontalEdge, SegmentType verticalEdge, Object childView,
+ Object parentView) {
+ IViewRule rule = loadRule(parent.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge,
+ childView, parentView);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent,
+ Rect newBounds, int modifierMask) {
+ IViewRule rule = loadRule(parent.getNode());
+
+ if (rule != null) {
+ try {
+ rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent,
+ Rect newBounds) {
+ IViewRule rule = loadRule(parent.getNode());
+
+ if (rule != null) {
+ try {
+ rule.onResizeEnd(feedback, child, parent, newBounds);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onResizeEnd() failed: %s", rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ // ---- Creation customizations ----
+
+ /**
+ * Invokes the create hooks ({@link IViewRule#onCreate},
+ * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and
+ * is inserted into a given parent. The parent may be null (for example when rendering
+ * top level items for preview).
+ *
+ * @param editor the XML editor to apply edits to the model for (performed by view
+ * rules)
+ * @param parentNode the parent XML node, or null if unknown
+ * @param childNode the XML node of the new node, never null
+ * @param overrideInsertType If not null, specifies an explicit insert type to use for
+ * edits made during the customization
+ */
+ public void callCreateHooks(
+ AndroidXmlEditor editor,
+ NodeProxy parentNode, NodeProxy childNode,
+ InsertType overrideInsertType) {
+ IViewRule parentRule = null;
+
+ if (parentNode != null) {
+ UiViewElementNode parentUiNode = parentNode.getNode();
+ parentRule = loadRule(parentUiNode);
+ }
+
+ if (overrideInsertType != null) {
+ mInsertType = overrideInsertType;
+ }
+
+ UiViewElementNode newUiNode = childNode.getNode();
+ IViewRule childRule = loadRule(newUiNode);
+ if (childRule != null || parentRule != null) {
+ callCreateHooks(editor, mInsertType, parentRule, parentNode,
+ childRule, childNode);
+ }
+ }
+
+ private static void callCreateHooks(
+ final AndroidXmlEditor editor, final InsertType insertType,
+ final IViewRule parentRule, final INode parentNode,
+ final IViewRule childRule, final INode newNode) {
+ // Notify the parent about the new child in case it wants to customize it
+ // (For example, a ScrollView parent can go and set all its children's layout params to
+ // fill the parent.)
+ if (!editor.isEditXmlModelPending()) {
+ editor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ callCreateHooks(editor, insertType,
+ parentRule, parentNode, childRule, newNode);
+ }
+ });
+ return;
+ }
+
+ if (parentRule != null) {
+ parentRule.onChildInserted(newNode, parentNode, insertType);
+ }
+
+ // Look up corresponding IViewRule, and notify the rule about
+ // this create action in case it wants to customize the new object.
+ // (For example, a rule for TabHosts can go and create a default child tab
+ // when you create it.)
+ if (childRule != null) {
+ childRule.onCreate(newNode, parentNode, insertType);
+ }
+
+ if (parentNode != null) {
+ ((NodeProxy) parentNode).applyPendingChanges();
+ }
+ }
+
+ /**
+ * Set the type of insert currently in progress
+ *
+ * @param insertType the insert type to use for the next operation
+ */
+ public void setInsertType(InsertType insertType) {
+ mInsertType = insertType;
+ }
+
+ /**
+ * Return the type of insert currently in progress
+ *
+ * @return the type of insert currently in progress
+ */
+ public InsertType getInsertType() {
+ return mInsertType;
+ }
+
+ // ---- Deletion ----
+
+ public void callOnRemovingChildren(NodeProxy parentNode,
+ List<INode> children) {
+ if (parentNode != null) {
+ UiViewElementNode parentUiNode = parentNode.getNode();
+ IViewRule parentRule = loadRule(parentUiNode);
+ if (parentRule != null) {
+ try {
+ parentRule.onRemovingChildren(children, parentNode,
+ mInsertType == InsertType.MOVE_WITHIN);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDispose() failed: %s",
+ parentRule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+ }
+
+ // ---- private ---
+
+ /**
+ * Returns the descriptor for the base View class.
+ * This could be null if the SDK or the given platform target hasn't loaded yet.
+ */
+ private ViewElementDescriptor getBaseViewDescriptor() {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mProject);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ return data.getLayoutDescriptors().getBaseViewDescriptor();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Clear the Rules cache. Calls onDispose() on each rule.
+ */
+ private void clearCache() {
+ // The cache can contain multiple times the same rule instance for different
+ // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer
+ // all values to a unique set.
+ HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values());
+
+ mRulesCache.clear();
+
+ for (IViewRule rule : rules) {
+ if (rule != null) {
+ try {
+ rule.onDispose();
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDispose() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks whether the project class loader has changed, and if so
+ * unregisters any view rules that use classes from the old class loader. It
+ * then returns the class loader to be used.
+ */
+ private ClassLoader updateClassLoader() {
+ ClassLoader classLoader = mRuleLoader.getClassLoader();
+ if (mUserClassLoader != null && classLoader != mUserClassLoader) {
+ // We have to unload all the IViewRules from the old class
+ List<Object> dispose = new ArrayList<Object>();
+ for (Map.Entry<Object, IViewRule> entry : mRulesCache.entrySet()) {
+ IViewRule rule = entry.getValue();
+ if (rule.getClass().getClassLoader() == mUserClassLoader) {
+ dispose.add(entry.getKey());
+ }
+ }
+ for (Object object : dispose) {
+ mRulesCache.remove(object);
+ }
+ }
+
+ mUserClassLoader = classLoader;
+ return mUserClassLoader;
+ }
+
+ /**
+ * Load a rule using its descriptor. This will try to first load the rule using its
+ * actual FQCN and if that fails will find the first parent that works in the view
+ * hierarchy.
+ */
+ private IViewRule loadRule(UiViewElementNode element) {
+ if (element == null) {
+ return null;
+ }
+
+ String targetFqcn = null;
+ ViewElementDescriptor targetDesc = null;
+
+ ElementDescriptor d = element.getDescriptor();
+ if (d instanceof ViewElementDescriptor) {
+ targetDesc = (ViewElementDescriptor) d;
+ }
+ if (d == null || !(d instanceof ViewElementDescriptor)) {
+ // This should not happen. All views should have some kind of *view* element
+ // descriptor. Maybe the project is not complete and doesn't build or something.
+ // In this case, we'll use the descriptor of the base android View class.
+ targetDesc = getBaseViewDescriptor();
+ }
+
+ // Check whether any of the custom view .jar files have changed and if so
+ // unregister previously cached view rules to force a new view rule to be loaded.
+ updateClassLoader();
+
+ // Return the rule if we find it in the cache, even if it was stored as null
+ // (which means we didn't find it earlier, so don't look for it again)
+ IViewRule rule = mRulesCache.get(targetDesc);
+ if (rule != null || mRulesCache.containsKey(targetDesc)) {
+ return rule;
+ }
+
+ // Get the descriptor and loop through the super class hierarchy
+ for (ViewElementDescriptor desc = targetDesc;
+ desc != null;
+ desc = desc.getSuperClassDesc()) {
+
+ // Get the FQCN of this View
+ String fqcn = desc.getFullClassName();
+ if (fqcn == null) {
+ // Shouldn't be happening.
+ return null;
+ }
+
+ // The first time we keep the FQCN around as it's the target class we were
+ // initially trying to load. After, as we move through the hierarchy, the
+ // target FQCN remains constant.
+ if (targetFqcn == null) {
+ targetFqcn = fqcn;
+ }
+
+ if (fqcn.indexOf('.') == -1) {
+ // Deal with unknown descriptors; these lack the full qualified path and
+ // elements in the layout without a package are taken to be in the
+ // android.widget package.
+ fqcn = ANDROID_WIDGET_PREFIX + fqcn;
+ }
+
+ // Try to find a rule matching the "real" FQCN. If we find it, we're done.
+ // If not, the for loop will move to the parent descriptor.
+ rule = loadRule(fqcn, targetFqcn);
+ if (rule != null) {
+ // We found one.
+ // As a side effect, loadRule() also cached the rule using the target FQCN.
+ return rule;
+ }
+ }
+
+ // Memorize in the cache that we couldn't find a rule for this descriptor
+ mRulesCache.put(targetDesc, null);
+ return null;
+ }
+
+ /**
+ * Try to load a rule given a specific FQCN. This looks for an exact match in either
+ * the ADT scripts or the project scripts and does not look at parent hierarchy.
+ * <p/>
+ * Once a rule is found (or not), it is stored in a cache using its target FQCN
+ * so we don't try to reload it.
+ * <p/>
+ * The real FQCN is the actual rule class we're loading, e.g. "android.view.View"
+ * where target FQCN is the class we were initially looking for, which might be the same as
+ * the real FQCN or might be a derived class, e.g. "android.widget.TextView".
+ *
+ * @param realFqcn The FQCN of the rule class actually being loaded.
+ * @param targetFqcn The FQCN of the class actually processed, which might be different from
+ * the FQCN of the rule being loaded.
+ */
+ IViewRule loadRule(String realFqcn, String targetFqcn) {
+ if (realFqcn == null || targetFqcn == null) {
+ return null;
+ }
+
+ // Return the rule if we find it in the cache, even if it was stored as null
+ // (which means we didn't find it earlier, so don't look for it again)
+ IViewRule rule = mRulesCache.get(realFqcn);
+ if (rule != null || mRulesCache.containsKey(realFqcn)) {
+ return rule;
+ }
+
+ // Look for class via reflection
+ try {
+ // For now, we package view rules for the builtin Android views and
+ // widgets with the tool in a special package, so look there rather
+ // than in the same package as the widgets.
+ String ruleClassName;
+ ClassLoader classLoader;
+ if (realFqcn.startsWith("android.") || //$NON-NLS-1$
+ realFqcn.equals(VIEW_MERGE) ||
+ realFqcn.endsWith(".GridLayout") || //$NON-NLS-1$ // Temporary special case
+ // FIXME: Remove this special case as soon as we pull
+ // the MapViewRule out of this code base and bundle it
+ // with the add ons
+ realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$
+ // This doesn't handle a case where there are name conflicts
+ // (e.g. where there are multiple different views with the same
+ // class name and only differing in package names, but that's a
+ // really bad practice in the first place, and if that situation
+ // should come up in the API we can enhance this algorithm.
+ String packageName = ViewRule.class.getName();
+ packageName = packageName.substring(0, packageName.lastIndexOf('.'));
+ classLoader = RulesEngine.class.getClassLoader();
+ int dotIndex = realFqcn.lastIndexOf('.');
+ String baseName = realFqcn.substring(dotIndex+1);
+ // Capitalize rule class name to match naming conventions, if necessary (<merge>)
+ if (Character.isLowerCase(baseName.charAt(0))) {
+ if (baseName.equals(VIEW_TAG)) {
+ // Hack: ViewRule is generic for the "View" class, so we can't use it
+ // for the special XML "view" tag (lowercase); instead, the rule is
+ // named "ViewTagRule" instead.
+ baseName = "ViewTag"; //$NON-NLS-1$
+ }
+ baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1);
+ }
+ ruleClassName = packageName + "." + //$NON-NLS-1$
+ baseName + "Rule"; //$NON-NLS-1$
+ } else {
+ // Initialize the user-classpath for 3rd party IViewRules, if necessary
+ classLoader = updateClassLoader();
+ if (classLoader == null) {
+ // The mUserClassLoader can be null; this is the typical scenario,
+ // when the user is only using builtin layout rules.
+ // This means however we can't resolve this fqcn since it's not
+ // in the name space of the builtin rules.
+ mRulesCache.put(realFqcn, null);
+ return null;
+ }
+
+ // For other (3rd party) widgets, look in the same package (though most
+ // likely not in the same jar!)
+ ruleClassName = realFqcn + "Rule"; //$NON-NLS-1$
+ }
+
+ Class<?> clz = Class.forName(ruleClassName, true, classLoader);
+ rule = (IViewRule) clz.newInstance();
+ return initializeRule(rule, targetFqcn);
+ } catch (ClassNotFoundException ex) {
+ // Not an unexpected error - this means that there isn't a helper for this
+ // class.
+ } catch (InstantiationException e) {
+ // This is NOT an expected error: fail.
+ AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString());
+ } catch (IllegalAccessException e) {
+ // This is NOT an expected error: fail.
+ AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString());
+ }
+
+ // Memorize in the cache that we couldn't find a rule for this real FQCN
+ mRulesCache.put(realFqcn, null);
+ return null;
+ }
+
+ /**
+ * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN
+ * and bail out.
+ * <p/>
+ * Contract: the rule is not in the {@link #mRulesCache} yet and this method will
+ * cache it using the target FQCN if the rule is accepted.
+ * <p/>
+ * The real FQCN is the actual rule class we're loading, e.g. "android.view.View"
+ * where target FQCN is the class we were initially looking for, which might be the same as
+ * the real FQCN or might be a derived class, e.g. "android.widget.TextView".
+ *
+ * @param rule A rule freshly loaded.
+ * @param targetFqcn The FQCN of the class actually processed, which might be different from
+ * the FQCN of the rule being loaded.
+ * @return The rule if accepted, or null if the rule can't handle that FQCN.
+ */
+ private IViewRule initializeRule(IViewRule rule, String targetFqcn) {
+
+ try {
+ if (rule.onInitialize(targetFqcn, new ClientRulesEngine(this, targetFqcn))) {
+ // Add it to the cache and return it
+ mRulesCache.put(targetFqcn, rule);
+ return rule;
+ } else {
+ rule.onDispose();
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onInit() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
new file mode 100644
index 000000000..5f2659ef2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.FQCN_BUTTON;
+import static com.android.SdkConstants.FQCN_SPINNER;
+import static com.android.SdkConstants.FQCN_TOGGLE_BUTTON;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.api.IViewMetadata.FillPreference;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.Density;
+import com.android.utils.Pair;
+import com.google.common.base.Splitter;
+import com.google.common.io.Closeables;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * The {@link ViewMetadataRepository} contains additional metadata for Android view
+ * classes
+ */
+public class ViewMetadataRepository {
+ private static final String PREVIEW_CONFIG_FILENAME = "rendering-configs.xml"; //$NON-NLS-1$
+ private static final String METADATA_FILENAME = "extra-view-metadata.xml"; //$NON-NLS-1$
+
+ /** Singleton instance */
+ private static ViewMetadataRepository sInstance = new ViewMetadataRepository();
+
+ /**
+ * Returns the singleton instance
+ *
+ * @return the {@link ViewMetadataRepository}
+ */
+ public static ViewMetadataRepository get() {
+ return sInstance;
+ }
+
+ /**
+ * Ever increasing counter used to assign natural ordering numbers to views and
+ * categories
+ */
+ private static int sNextOrdinal = 0;
+
+ /**
+ * List of categories (which contain views); constructed lazily so use
+ * {@link #getCategories()}
+ */
+ private List<CategoryData> mCategories;
+
+ /**
+ * Map from class names to view data objects; constructed lazily so use
+ * {@link #getClassToView}
+ */
+ private Map<String, ViewData> mClassToView;
+
+ /** Hidden constructor: Create via factory {@link #get()} instead */
+ private ViewMetadataRepository() {
+ }
+
+ /** Returns a map from class fully qualified names to {@link ViewData} objects */
+ private Map<String, ViewData> getClassToView() {
+ if (mClassToView == null) {
+ int initialSize = 75;
+ mClassToView = new HashMap<String, ViewData>(initialSize);
+ List<CategoryData> categories = getCategories();
+ for (CategoryData category : categories) {
+ for (ViewData view : category) {
+ mClassToView.put(view.getFcqn(), view);
+ }
+ }
+ assert mClassToView.size() <= initialSize;
+ }
+
+ return mClassToView;
+ }
+
+ /**
+ * Returns an XML document containing rendering configurations for the various Android
+ * views. The FQN of each view can be obtained via the
+ * {@link #getFullClassName(Element)} method
+ *
+ * @return an XML document containing rendering elements
+ */
+ public Document getRenderingConfigDoc() {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ Class<ViewMetadataRepository> clz = ViewMetadataRepository.class;
+ InputStream paletteStream = clz.getResourceAsStream(PREVIEW_CONFIG_FILENAME);
+ InputSource is = new InputSource(paletteStream);
+ try {
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ factory.setIgnoringComments(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ return builder.parse(is);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Parsing palette file failed");
+ return null;
+ } finally {
+ Closeables.closeQuietly(paletteStream);
+ }
+ }
+
+ /**
+ * Returns a fully qualified class name for an element in the rendering document
+ * returned by {@link #getRenderingConfigDoc()}
+ *
+ * @param element the element to look up the fqcn for
+ * @return the fqcn of the view the element represents a preview for
+ */
+ public String getFullClassName(Element element) {
+ // We don't use the element tag name, because in some cases we have
+ // an outer element to render some interesting inner element, such as a tab widget
+ // (which must be rendered inside a tab host).
+ //
+ // Therefore, we instead use the convention that the id is the fully qualified
+ // class name, with .'s replaced with _'s.
+
+ // Special case: for tab host we aren't allowed to mess with the id
+ String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+
+ if ("@android:id/tabhost".equals(id)) {
+ // Special case to distinguish TabHost and TabWidget
+ NodeList children = element.getChildNodes();
+ if (children.getLength() > 1 && (children.item(1) instanceof Element)) {
+ Element child = (Element) children.item(1);
+ String childId = child.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if ("@+id/android_widget_TabWidget".equals(childId)) {
+ return "android.widget.TabWidget"; // TODO: Tab widget!
+ }
+ }
+ return "android.widget.TabHost"; // TODO: Tab widget!
+ }
+
+ StringBuilder sb = new StringBuilder();
+ int i = 0;
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ i = NEW_ID_PREFIX.length();
+ } else if (id.startsWith(ID_PREFIX)) {
+ i = ID_PREFIX.length();
+ }
+
+ for (; i < id.length(); i++) {
+ char c = id.charAt(i);
+ if (c == '_') {
+ sb.append('.');
+ } else {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /** Returns an ordered list of categories and views, parsed from a metadata file */
+ @SuppressWarnings("resource") // streams passed to parser InputSource closed by parser
+ private List<CategoryData> getCategories() {
+ if (mCategories == null) {
+ mCategories = new ArrayList<CategoryData>();
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ Class<ViewMetadataRepository> clz = ViewMetadataRepository.class;
+ InputStream inputStream = clz.getResourceAsStream(METADATA_FILENAME);
+ InputSource is = new InputSource(new BufferedInputStream(inputStream));
+ try {
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ factory.setIgnoringComments(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document document = builder.parse(is);
+ Map<String, FillPreference> fillTypes = new HashMap<String, FillPreference>();
+ for (FillPreference pref : FillPreference.values()) {
+ fillTypes.put(pref.toString().toLowerCase(Locale.US), pref);
+ }
+
+ NodeList categoryNodes = document.getDocumentElement().getChildNodes();
+ for (int i = 0, n = categoryNodes.getLength(); i < n; i++) {
+ Node node = categoryNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ if (element.getNodeName().equals("category")) { //$NON-NLS-1$
+ String name = element.getAttribute("name"); //$NON-NLS-1$
+ CategoryData category = new CategoryData(name);
+ NodeList children = element.getChildNodes();
+ for (int j = 0, m = children.getLength(); j < m; j++) {
+ Node childNode = children.item(j);
+ if (childNode.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) childNode;
+ ViewData view = createViewData(fillTypes, child,
+ null, FillPreference.NONE, RenderMode.NORMAL, null);
+ category.addView(view);
+ }
+ }
+ mCategories.add(category);
+ }
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Invalid palette metadata"); //$NON-NLS-1$
+ }
+ }
+
+ return mCategories;
+ }
+
+ private ViewData createViewData(Map<String, FillPreference> fillTypes,
+ Element child, String defaultFqcn, FillPreference defaultFill,
+ RenderMode defaultRender, String defaultSize) {
+ String fqcn = child.getAttribute("class"); //$NON-NLS-1$
+ if (fqcn.length() == 0) {
+ fqcn = defaultFqcn;
+ }
+ String fill = child.getAttribute("fill"); //$NON-NLS-1$
+ FillPreference fillPreference = null;
+ if (fill.length() > 0) {
+ fillPreference = fillTypes.get(fill);
+ }
+ if (fillPreference == null) {
+ fillPreference = defaultFill;
+ }
+ String skip = child.getAttribute("skip"); //$NON-NLS-1$
+ RenderMode renderMode = defaultRender;
+ String render = child.getAttribute("render"); //$NON-NLS-1$
+ if (render.length() > 0) {
+ renderMode = RenderMode.get(render);
+ }
+ String displayName = child.getAttribute("name"); //$NON-NLS-1$
+ if (displayName.length() == 0) {
+ displayName = null;
+ }
+
+ String relatedTo = child.getAttribute("relatedTo"); //$NON-NLS-1$
+ String topAttrs = child.getAttribute("topAttrs"); //$NON-NLS-1$
+ String resize = child.getAttribute("resize"); //$NON-NLS-1$
+ ViewData view = new ViewData(fqcn, displayName, fillPreference,
+ skip.length() == 0 ? false : Boolean.valueOf(skip),
+ renderMode, relatedTo, resize, topAttrs);
+
+ String init = child.getAttribute("init"); //$NON-NLS-1$
+ String icon = child.getAttribute("icon"); //$NON-NLS-1$
+
+ view.setInitString(init);
+ if (icon.length() > 0) {
+ view.setIconName(icon);
+ }
+
+ // Nested variations?
+ if (child.hasChildNodes()) {
+ // Palette variations
+ NodeList childNodes = child.getChildNodes();
+ for (int k = 0, kl = childNodes.getLength(); k < kl; k++) {
+ Node variationNode = childNodes.item(k);
+ if (variationNode.getNodeType() == Node.ELEMENT_NODE) {
+ Element variation = (Element) variationNode;
+ ViewData variationView = createViewData(fillTypes, variation,
+ fqcn, fillPreference, renderMode, resize);
+ view.addVariation(variationView);
+ }
+ }
+ }
+
+ return view;
+ }
+
+ /**
+ * Computes the palette entries for the given {@link AndroidTargetData}, looking up the
+ * available node descriptors, categorizing and sorting them.
+ *
+ * @param targetData the target data for which to compute palette entries
+ * @param alphabetical if true, sort all items in alphabetical order
+ * @param createCategories if true, organize the items into categories
+ * @return a list of pairs where each pair contains of the category label and an
+ * ordered list of elements to be included in that category
+ */
+ public List<Pair<String, List<ViewElementDescriptor>>> getPaletteEntries(
+ AndroidTargetData targetData, boolean alphabetical, boolean createCategories) {
+ List<Pair<String, List<ViewElementDescriptor>>> result =
+ new ArrayList<Pair<String, List<ViewElementDescriptor>>>();
+
+ List<List<ViewElementDescriptor>> lists = new ArrayList<List<ViewElementDescriptor>>(2);
+ LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors();
+ lists.add(layoutDescriptors.getViewDescriptors());
+ lists.add(layoutDescriptors.getLayoutDescriptors());
+
+ // First record map of FQCN to ViewElementDescriptor such that we can quickly
+ // determine if a particular palette entry is available
+ Map<String, ViewElementDescriptor> fqcnToDescriptor =
+ new HashMap<String, ViewElementDescriptor>();
+ for (List<ViewElementDescriptor> list : lists) {
+ for (ViewElementDescriptor view : list) {
+ String fqcn = view.getFullClassName();
+ if (fqcn == null) {
+ // <view> and <merge> tags etc
+ fqcn = view.getUiName();
+ }
+ fqcnToDescriptor.put(fqcn, view);
+ }
+ }
+
+ Set<ViewElementDescriptor> remaining = new HashSet<ViewElementDescriptor>(
+ layoutDescriptors.getViewDescriptors().size()
+ + layoutDescriptors.getLayoutDescriptors().size());
+ remaining.addAll(layoutDescriptors.getViewDescriptors());
+ remaining.addAll(layoutDescriptors.getLayoutDescriptors());
+
+ // Now iterate in palette metadata order over the items in the palette and include
+ // any that also appear as a descriptor
+ List<ViewElementDescriptor> categoryItems = new ArrayList<ViewElementDescriptor>();
+ for (CategoryData category : getCategories()) {
+ if (createCategories) {
+ categoryItems = new ArrayList<ViewElementDescriptor>();
+ }
+ for (ViewData view : category) {
+ String fqcn = view.getFcqn();
+ ViewElementDescriptor descriptor = fqcnToDescriptor.get(fqcn);
+ if (descriptor != null) {
+ remaining.remove(descriptor);
+ if (view.getSkip()) {
+ continue;
+ }
+
+ if (view.getDisplayName() != null || view.getInitString().length() > 0) {
+ categoryItems.add(new PaletteMetadataDescriptor(descriptor,
+ view.getDisplayName(), view.getInitString(), view.getIconName()));
+ } else {
+ categoryItems.add(descriptor);
+ }
+
+ if (view.hasVariations()) {
+ for (ViewData variation : view.getVariations()) {
+ String init = variation.getInitString();
+ String icon = variation.getIconName();
+ ViewElementDescriptor desc = new PaletteMetadataDescriptor(descriptor,
+ variation.getDisplayName(), init, icon);
+ categoryItems.add(desc);
+ }
+ }
+ }
+ }
+
+ if (createCategories && categoryItems.size() > 0) {
+ if (alphabetical) {
+ Collections.sort(categoryItems);
+ }
+ result.add(Pair.of(category.getName(), categoryItems));
+ }
+ }
+
+ if (remaining.size() > 0) {
+ List<ViewElementDescriptor> otherItems =
+ new ArrayList<ViewElementDescriptor>(remaining);
+ // Always sorted, we don't have a natural order for these unknowns
+ Collections.sort(otherItems);
+ if (createCategories) {
+ result.add(Pair.of("Other", otherItems));
+ } else {
+ categoryItems.addAll(otherItems);
+ }
+ }
+
+ if (!createCategories) {
+ if (alphabetical) {
+ Collections.sort(categoryItems);
+ }
+ result.add(Pair.of("Views", categoryItems));
+ }
+
+ return result;
+ }
+
+ @VisibleForTesting
+ Collection<String> getAllFqcns() {
+ return getClassToView().keySet();
+ }
+
+ /**
+ * Metadata holder for a particular category - contains the name of the category, its
+ * ordinal (for natural/logical sorting order) and views contained in the category
+ */
+ private static class CategoryData implements Iterable<ViewData>, Comparable<CategoryData> {
+ /** Category name */
+ private final String mName;
+ /** Views included in this category */
+ private final List<ViewData> mViews = new ArrayList<ViewData>();
+ /** Natural ordering rank */
+ private final int mOrdinal = sNextOrdinal++;
+
+ /** Constructs a new category with the given name */
+ private CategoryData(String name) {
+ super();
+ mName = name;
+ }
+
+ /** Adds a new view into this category */
+ private void addView(ViewData view) {
+ mViews.add(view);
+ }
+
+ private String getName() {
+ return mName;
+ }
+
+ // Implements Iterable<ViewData> such that we can use for-each on the category to
+ // enumerate its views
+ @Override
+ public Iterator<ViewData> iterator() {
+ return mViews.iterator();
+ }
+
+ // Implements Comparable<CategoryData> such that categories can be naturally sorted
+ @Override
+ public int compareTo(CategoryData other) {
+ return mOrdinal - other.mOrdinal;
+ }
+ }
+
+ /** Metadata holder for a view of a given fully qualified class name */
+ private static class ViewData implements Comparable<ViewData> {
+ /** The fully qualified class name of the view */
+ private final String mFqcn;
+ /** Fill preference of the view */
+ private final FillPreference mFillPreference;
+ /** Skip this item in the palette? */
+ private final boolean mSkip;
+ /** Must this item be rendered alone? skipped? etc */
+ private final RenderMode mRenderMode;
+ /** Related views */
+ private final String mRelatedTo;
+ /** The relative rank of the view for natural ordering */
+ private final int mOrdinal = sNextOrdinal++;
+ /** List of optional variations */
+ private List<ViewData> mVariations;
+ /** Display name. Can be null. */
+ private String mDisplayName;
+ /**
+ * Optional initialization string - a comma separate set of name/value pairs to
+ * initialize the element with
+ */
+ private String mInitString;
+ /** The name of an icon (known to the {@link IconFactory} to show for this view */
+ private String mIconName;
+ /** The resize preference of this view */
+ private String mResize;
+ /** The most commonly set attributes of this view */
+ private String mTopAttrs;
+
+ /** Constructs a new view data for the given class */
+ private ViewData(String fqcn, String displayName,
+ FillPreference fillPreference, boolean skip, RenderMode renderMode,
+ String relatedTo, String resize, String topAttrs) {
+ super();
+ mFqcn = fqcn;
+ mDisplayName = displayName;
+ mFillPreference = fillPreference;
+ mSkip = skip;
+ mRenderMode = renderMode;
+ mRelatedTo = relatedTo;
+ mResize = resize;
+ mTopAttrs = topAttrs;
+ }
+
+ /** Returns the {@link FillPreference} for views of this type */
+ private FillPreference getFillPreference() {
+ return mFillPreference;
+ }
+
+ /** Fully qualified class name of views of this type */
+ private String getFcqn() {
+ return mFqcn;
+ }
+
+ private String getDisplayName() {
+ return mDisplayName;
+ }
+
+ private String getResize() {
+ return mResize;
+ }
+
+ // Implements Comparable<ViewData> such that views can be sorted naturally
+ @Override
+ public int compareTo(ViewData other) {
+ return mOrdinal - other.mOrdinal;
+ }
+
+ public RenderMode getRenderMode() {
+ return mRenderMode;
+ }
+
+ public boolean getSkip() {
+ return mSkip;
+ }
+
+ public List<String> getRelatedTo() {
+ if (mRelatedTo == null || mRelatedTo.length() == 0) {
+ return Collections.emptyList();
+ } else {
+ List<String> result = new ArrayList<String>();
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ Map<String, ViewData> classToView = repository.getClassToView();
+
+ List<String> fqns = new ArrayList<String>(classToView.keySet());
+ for (String basename : Splitter.on(',').split(mRelatedTo)) {
+ boolean found = false;
+ for (String fqcn : fqns) {
+ String suffix = '.' + basename;
+ if (fqcn.endsWith(suffix)) {
+ result.add(fqcn);
+ found = true;
+ break;
+ }
+ }
+ if (basename.equals(VIEW_FRAGMENT) || basename.equals(VIEW_INCLUDE)) {
+ result.add(basename);
+ } else {
+ assert found : basename;
+ }
+ }
+
+ return result;
+ }
+ }
+
+ public List<String> getTopAttributes() {
+ // "id" is a top attribute for all views, so it is not included in the XML, we just
+ // add it in dynamically here
+ if (mTopAttrs == null || mTopAttrs.length() == 0) {
+ return Collections.singletonList(ATTR_ID);
+ } else {
+ String[] split = mTopAttrs.split(","); //$NON-NLS-1$
+ List<String> topAttributes = new ArrayList<String>(split.length + 1);
+ topAttributes.add(ATTR_ID);
+ for (int i = 0, n = split.length; i < n; i++) {
+ topAttributes.add(split[i]);
+ }
+ return Collections.<String>unmodifiableList(topAttributes);
+ }
+ }
+
+ void addVariation(ViewData variation) {
+ if (mVariations == null) {
+ mVariations = new ArrayList<ViewData>(4);
+ }
+ mVariations.add(variation);
+ }
+
+ List<ViewData> getVariations() {
+ return mVariations;
+ }
+
+ boolean hasVariations() {
+ return mVariations != null && mVariations.size() > 0;
+ }
+
+ private void setInitString(String initString) {
+ this.mInitString = initString;
+ }
+
+ private String getInitString() {
+ return mInitString;
+ }
+
+ private void setIconName(String iconName) {
+ this.mIconName = iconName;
+ }
+
+ private String getIconName() {
+ return mIconName;
+ }
+ }
+
+ /**
+ * Returns the {@link FillPreference} for classes with the given fully qualified class
+ * name
+ *
+ * @param fqcn the fully qualified class name of the view
+ * @return a suitable {@link FillPreference} for the given view type
+ */
+ public FillPreference getFillPreference(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ return view.getFillPreference();
+ }
+
+ return FillPreference.NONE;
+ }
+
+ /**
+ * Returns the {@link RenderMode} for classes with the given fully qualified class
+ * name
+ *
+ * @param fqcn the fully qualified class name
+ * @return the {@link RenderMode} to use for previews of the given view type
+ */
+ public RenderMode getRenderMode(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ return view.getRenderMode();
+ }
+
+ return RenderMode.NORMAL;
+ }
+
+ /**
+ * Returns the {@link ResizePolicy} for the given class.
+ *
+ * @param fqcn the fully qualified class name of the target widget
+ * @return the {@link ResizePolicy} for the widget, which will never be null (but may
+ * be the default of {@link ResizePolicy#full()} if no metadata is found for
+ * the given widget)
+ */
+ public ResizePolicy getResizePolicy(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ String resize = view.getResize();
+ if (resize != null && resize.length() > 0) {
+ if ("full".equals(resize)) { //$NON-NLS-1$
+ return ResizePolicy.full();
+ } else if ("none".equals(resize)) { //$NON-NLS-1$
+ return ResizePolicy.none();
+ } else if ("horizontal".equals(resize)) { //$NON-NLS-1$
+ return ResizePolicy.horizontal();
+ } else if ("vertical".equals(resize)) { //$NON-NLS-1$
+ return ResizePolicy.vertical();
+ } else if ("scaled".equals(resize)) { //$NON-NLS-1$
+ return ResizePolicy.scaled();
+ } else {
+ assert false : resize;
+ }
+ }
+ }
+
+ return ResizePolicy.full();
+ }
+
+ /**
+ * Returns true if classes with the given fully qualified class name should be hidden
+ * or skipped from the palette
+ *
+ * @param fqcn the fully qualified class name
+ * @return true if views of the given type should be hidden from the palette
+ */
+ public boolean getSkip(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ return view.getSkip();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a list of the top (most commonly set) attributes of the given
+ * view.
+ *
+ * @param fqcn the fully qualified class name
+ * @return a list, never null but possibly empty, of popular attribute names
+ * (not including a namespace prefix)
+ */
+ public List<String> getTopAttributes(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ return view.getTopAttributes();
+ }
+
+ return Collections.singletonList(ATTR_ID);
+ }
+
+ /**
+ * Returns a set of fully qualified names for views that are closely related to the
+ * given view
+ *
+ * @param fqcn the fully qualified class name
+ * @return a list, never null but possibly empty, of views that are related to the
+ * view of the given type
+ */
+ public List<String> getRelatedTo(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ return view.getRelatedTo();
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Render mode for palette preview */
+ public enum RenderMode {
+ /**
+ * Render previews, and it can be rendered as a sibling of many other views in a
+ * big linear layout
+ */
+ NORMAL,
+ /** This view needs to be rendered alone */
+ ALONE,
+ /**
+ * Skip this element; it doesn't work or does not produce any visible artifacts
+ * (such as the basic layouts)
+ */
+ SKIP;
+
+ /**
+ * Returns the {@link RenderMode} for the given render XML attribute
+ * value
+ *
+ * @param render the attribute value in the metadata XML file
+ * @return a corresponding {@link RenderMode}, never null
+ */
+ public static RenderMode get(String render) {
+ if ("alone".equals(render)) { //$NON-NLS-1$
+ return ALONE;
+ } else if ("skip".equals(render)) { //$NON-NLS-1$
+ return SKIP;
+ } else {
+ return NORMAL;
+ }
+ }
+ }
+
+ /**
+ * Are insets supported yet? This flag indicates whether the {@link #getInsets} method
+ * can return valid data, such that clients can avoid doing any work computing the
+ * current theme or density if there's no chance that valid insets will be returned
+ */
+ public static final boolean INSETS_SUPPORTED = false;
+
+ /**
+ * Returns the insets of widgets with the given fully qualified name, in the given
+ * theme and the given screen density.
+ *
+ * @param fqcn the fully qualified name of the view
+ * @param density the screen density
+ * @param theme the theme name
+ * @return the insets of the visual bounds relative to the view info bounds, or null
+ * if not known or if there are no insets
+ */
+ public static Margins getInsets(String fqcn, Density density, String theme) {
+ if (INSETS_SUPPORTED) {
+ // Some sample data measured manually for common themes and widgets.
+ if (fqcn.equals(FQCN_BUTTON)) {
+ if (density == Density.HIGH) {
+ if (theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Holo, Theme.Holo.Light, WVGA
+ return new Margins(5, 5, 5, 5);
+ } else {
+ // Theme.Light, WVGA
+ return new Margins(4, 4, 0, 7);
+ }
+ } else if (density == Density.MEDIUM) {
+ if (theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Holo, Theme.Holo.Light, WVGA
+ return new Margins(3, 3, 3, 3);
+ } else {
+ // Theme.Light, HVGA
+ return new Margins(2, 2, 0, 4);
+ }
+ } else if (density == Density.LOW) {
+ if (theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Holo, Theme.Holo.Light, QVGA
+ return new Margins(2, 2, 2, 2);
+ } else {
+ // Theme.Light, QVGA
+ return new Margins(1, 3, 0, 4);
+ }
+ }
+ } else if (fqcn.equals(FQCN_TOGGLE_BUTTON)) {
+ if (density == Density.HIGH) {
+ if (theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Holo, Theme.Holo.Light, WVGA
+ return new Margins(5, 5, 5, 5);
+ } else {
+ // Theme.Light, WVGA
+ return new Margins(2, 2, 0, 5);
+ }
+ } else if (density == Density.MEDIUM) {
+ if (theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Holo, Theme.Holo.Light, WVGA
+ return new Margins(3, 3, 3, 3);
+ } else {
+ // Theme.Light, HVGA
+ return new Margins(0, 1, 0, 3);
+ }
+ } else if (density == Density.LOW) {
+ if (theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Holo, Theme.Holo.Light, QVGA
+ return new Margins(2, 2, 2, 2);
+ } else {
+ // Theme.Light, QVGA
+ return new Margins(2, 2, 0, 4);
+ }
+ }
+ } else if (fqcn.equals(FQCN_SPINNER)) {
+ if (density == Density.HIGH) {
+ if (!theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Light, WVGA
+ return new Margins(3, 4, 2, 8);
+ } // Doesn't render on Holo!
+ } else if (density == Density.MEDIUM) {
+ if (!theme.startsWith(HOLO_PREFIX)) {
+ // Theme.Light, HVGA
+ return new Margins(1, 1, 0, 4);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static final String HOLO_PREFIX = "Theme.Holo"; //$NON-NLS-1$
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
new file mode 100644
index 000000000..6a67b1db4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
@@ -0,0 +1,452 @@
+<?xml version="1.0" encoding="UTF-8"?>
+ <!--
+ Palette Metadata
+
+ This document provides additional designtime metadata for various Android views, such as
+ logical palette categories (as well as a natural ordering of the views within their
+ categories, fill-preferences (how a view will sets its width and height attributes when
+ dropped into other views), and so on.
+ -->
+<!DOCTYPE metadata [
+<!--- The metadata consists of a series of category definitions -->
+<!ELEMENT metadata (category)*>
+<!--- Each category has a name and contains a list of views in order -->
+<!ELEMENT category (view)*>
+<!ATTLIST category name CDATA #IMPLIED>
+<!--- Each view is identified by its full class name and has various
+ other attributes such as a fill preference -->
+<!ELEMENT view (view)*>
+<!ATTLIST view
+ class CDATA #IMPLIED
+ name CDATA #IMPLIED
+ init CDATA #IMPLIED
+ icon CDATA #IMPLIED
+ relatedTo CDATA #IMPLIED
+ skip (true|false) "false"
+ render (alone|skip|normal) "normal"
+ fill (none|both|width|height|opposite|width_in_vertical|height_in_horizontal) "none"
+ resize (full|none|horizontal|vertical|scaled) "full"
+ topAttrs CDATA #IMPLIED
+>
+]>
+<metadata>
+ <category
+ name="Form Widgets">
+ <view
+ class="android.widget.TextView"
+ topAttrs="text,textAppearance,textColor,textSize"
+ name="TextView"
+ init=""
+ relatedTo="EditText,AutoCompleteTextView,MultiAutoCompleteTextView">
+ <view
+ name="Large Text"
+ init="android:textAppearance=?android:attr/textAppearanceLarge,android:text=Large Text" />
+ <view
+ name="Medium Text"
+ init="android:textAppearance=?android:attr/textAppearanceMedium,android:text=Medium Text" />
+ <view
+ name="Small Text"
+ init="android:textAppearance=?android:attr/textAppearanceSmall,android:text=Small Text" />
+ </view>
+ <view
+ class="android.widget.Button"
+ topAttrs="text,style"
+ name="Button"
+ init=""
+ relatedTo="ImageButton">
+ <view
+ name="Small Button"
+ init="style=?android:attr/buttonStyleSmall,android:text=Button" />
+ </view>
+ <view
+ class="android.widget.ToggleButton"
+ topAttrs="textOff,textOn,style,background"
+ relatedTo="CheckBox" />
+ <view
+ class="android.widget.CheckBox"
+ topAttrs="text"
+ relatedTo="RadioButton,ToggleButton,CheckedTextView" />
+ <view
+ class="android.widget.RadioButton"
+ topAttrs="text,style"
+ relatedTo="CheckBox,ToggleButton" />
+ <view
+ class="android.widget.CheckedTextView"
+ topAttrs="gravity,paddingLeft,paddingRight,checkMark,textAppearance"
+ relatedTo="TextView,CheckBox" />
+ <view
+ class="android.widget.Spinner"
+ topAttrs="prompt,entries,style"
+ relatedTo="EditText"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.ProgressBar"
+ topAttrs="style,visibility,indeterminate,max"
+ relatedTo="SeekBar"
+ name="ProgressBar (Large)"
+ init="style=?android:attr/progressBarStyleLarge"
+ resize="scaled" >
+ <view
+ name="ProgressBar (Normal)"
+ init=""
+ resize="scaled" />
+ <view
+ name="ProgressBar (Small)"
+ init="style=?android:attr/progressBarStyleSmall"
+ resize="scaled" />
+ <view
+ name="ProgressBar (Horizontal)"
+ init="style=?android:attr/progressBarStyleHorizontal"
+ resize="horizontal" />
+ </view>
+ <view
+ class="android.widget.SeekBar"
+ topAttrs="paddingLeft,paddingRight,progressDrawable,thumb"
+ relatedTo="ProgressBar"
+ resize="horizontal"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.QuickContactBadge"
+ topAttrs="src,style,gravity"
+ resize="scaled" />
+ <view
+ class="android.widget.RadioGroup"
+ topAttrs="orientation,paddingBottom,paddingTop,style" />
+ <view
+ class="android.widget.RatingBar"
+ topAttrs="numStars,stepSize,style,isIndicator"
+ resize="horizontal" />
+ <view
+ class="android.widget.Switch"
+ topAttrs="text,textOff,textOn,style,checked"
+ relatedTo="CheckBox,ToggleButton"
+ render="alone" />
+ </category>
+ <category
+ name="Text Fields">
+ <view
+ class="android.widget.EditText"
+ topAttrs="hint,inputType,singleLine"
+ name="Plain Text"
+ init=""
+ resize="full"
+ relatedTo="Spinner,TextView,AutoCompleteTextView,MultiAutoCompleteTextView"
+ fill="width_in_vertical">
+ <view
+ name="Person Name"
+ init="android:inputType=textPersonName" />
+ <view
+ name="Password"
+ init="android:inputType=textPassword" />
+ <view
+ name="Password (Numeric)"
+ init="android:inputType=numberPassword" />
+ <view
+ name="E-mail"
+ init="android:inputType=textEmailAddress" />
+ <view
+ name="Phone"
+ init="android:inputType=phone" />
+ <view
+ name="Postal Address"
+ resize="full"
+ init="android:inputType=textPostalAddress" />
+ <view
+ name="Multiline Text"
+ resize="full"
+ init="android:inputType=textMultiLine" />
+ <view
+ name="Time"
+ init="android:inputType=time" />
+ <view
+ name="Date"
+ init="android:inputType=date" />
+ <view
+ name="Number"
+ init="android:inputType=number" />
+ <view
+ name="Number (Signed)"
+ init="android:inputType=numberSigned" />
+ <view
+ name="Number (Decimal)"
+ init="android:inputType=numberDecimal" />
+ </view>
+ <view
+ class="android.widget.AutoCompleteTextView"
+ topAttrs="singleLine,autoText"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.MultiAutoCompleteTextView"
+ topAttrs="background,hint,imeOptions,inputType,style,textColor"
+ fill="width_in_vertical" />
+ </category>
+ <category
+ name="Layouts">
+ <view
+ class="android.widget.GridLayout"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.LinearLayout"
+ topAttrs="orientation,gravity"
+ name="LinearLayout (Vertical)"
+ init="android:orientation=vertical"
+ icon="VerticalLinearLayout"
+ fill="opposite"
+ render="skip">
+ <view
+ name="LinearLayout (Horizontal)" />
+ </view>
+ <view
+ class="android.widget.RelativeLayout"
+ topAttrs="background,orientation,paddingLeft"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.FrameLayout"
+ topAttrs="background"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="include"
+ topAttrs="layout"
+ name="Include Other Layout"
+ render="skip"
+ relatedTo="fragment" />
+ <view
+ class="fragment"
+ topAttrs="class,name"
+ name="Fragment"
+ fill="opposite"
+ render="skip"
+ relatedTo="include" />
+ <view
+ class="android.widget.TableLayout"
+ topAttrs="stretchColumns,shrinkColumns,orientation"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.TableRow"
+ topAttrs="paddingTop,focusable,gravity,visibility"
+ fill="opposite"
+ resize="vertical"
+ render="skip" />
+ <view
+ class="android.widget.Space"
+ fill="opposite"
+ render="skip" />
+ </category>
+ <category
+ name="Composite">
+ <view
+ class="android.widget.ListView"
+ topAttrs="drawSelectorOnTop,cacheColorHint,divider,background"
+ relatedTo="ExpandableListView"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.ExpandableListView"
+ topAttrs="drawSelectorOnTop,cacheColorHint,indicatorLeft,indicatorRight,scrollbars,textSize"
+ relatedTo="ListView"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.GridView"
+ topAttrs="numColumns,verticalSpacing,horizontalSpacing"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ScrollView"
+ topAttrs="fillViewport,orientation,scrollbars"
+ relatedTo="HorizontalScrollView"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.HorizontalScrollView"
+ topAttrs="scrollbars,fadingEdgeLength,fadingEdge"
+ relatedTo="ScrollView"
+ render="skip" />
+ <view
+ class="android.widget.SearchView"
+ topAttrs="iconifiedByDefault,queryHint,maxWidth,minWidth,visibility"
+ render="skip" />
+ <view
+ class="android.widget.SlidingDrawer"
+ render="skip"
+ topAttrs="allowSingleTap,bottomOffset,content,handle,topOffset,visibility" />
+ <view
+ class="android.widget.TabHost"
+ topAttrs="paddingTop,background,duplicateParentState,visibility"
+ fill="width_in_vertical"
+ render="alone" />
+ <view
+ class="android.widget.TabWidget"
+ topAttrs="background,paddingLeft,tabStripEnabled,gravity"
+ render="alone" />
+ <view
+ class="android.webkit.WebView"
+ topAttrs="background,visibility,textAppearance"
+ fill="opposite"
+ render="skip" />
+ </category>
+ <category
+ name="Images &amp; Media">
+ <view
+ class="android.widget.ImageView"
+ topAttrs="src,scaleType"
+ resize="scaled"
+ render="skip"
+ relatedTo="ImageButton,VideoView" />
+ <view
+ class="android.widget.ImageButton"
+ topAttrs="src,background,style"
+ resize="scaled"
+ render="skip"
+ relatedTo="Button,ImageView" />
+ <view
+ class="android.widget.Gallery"
+ topAttrs="gravity,spacing,background"
+ fill="width_in_vertical"
+ render="skip" />
+ <view
+ class="android.widget.MediaController"
+ render="skip" />
+ <view
+ class="android.widget.VideoView"
+ relatedTo="ImageView"
+ fill="opposite"
+ render="skip" />
+ </category>
+ <category
+ name="Time &amp; Date">
+ <view
+ class="android.widget.TimePicker"
+ topAttrs="visibility"
+ relatedTo="DatePicker,CalendarView"
+ render="alone" />
+ <view
+ class="android.widget.DatePicker"
+ relatedTo="TimePicker"
+ render="alone" />
+ <view
+ class="android.widget.CalendarView"
+ topAttrs="focusable,focusableInTouchMode,visibility"
+ fill="both"
+ relatedTo="TimePicker,DatePicker" />
+ <view
+ class="android.widget.Chronometer"
+ topAttrs="textSize,gravity,visibility"
+ render="skip" />
+ <view
+ class="android.widget.AnalogClock"
+ topAttrs="dial,hand_hour,hand_minute"
+ relatedTo="DigitalClock" />
+ <view
+ class="android.widget.DigitalClock"
+ relatedTo="AnalogClock" />
+ </category>
+ <category
+ name="Transitions">
+ <view
+ class="android.widget.ImageSwitcher"
+ topAttrs="inAnimation,outAnimation,cropToPadding,padding,scaleType"
+ relatedTo="ViewFlipper,ViewSwitcher,TextSwitcher"
+ render="skip" />
+ <view
+ class="android.widget.AdapterViewFlipper"
+ topAttrs="autoStart,flipInterval,inAnimation,outAnimation"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.StackView"
+ topAttrs="loopViews,gravity"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.TextSwitcher"
+ relatedTo="ViewFlipper,ImageSwitcher,ViewSwitcher"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ViewAnimator"
+ topAttrs="inAnimation,outAnimation"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ViewFlipper"
+ topAttrs="flipInterval,inAnimation,outAnimation,addStatesFromChildren,measureAllChildren"
+ relatedTo="ViewSwitcher,ImageSwitcher,TextSwitcher"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ViewSwitcher"
+ topAttrs="inAnimation,outAnimation"
+ relatedTo="ViewFlipper,ImageSwitcher,TextSwitcher"
+ fill="opposite"
+ render="skip" />
+ </category>
+ <category
+ name="Advanced">
+ <view
+ class="requestFocus"
+ render="skip" />
+ <view
+ class="android.view.View"
+ topAttrs="background,visibility,style"
+ render="skip" />
+ <view
+ class="android.view.ViewStub"
+ topAttrs="layout,inflatedId,visibility"
+ render="skip" />
+ <view
+ class="view"
+ topAttrs="class"
+ render="skip" />
+ <view
+ class="android.gesture.GestureOverlayView"
+ topAttrs="gestureStrokeType,uncertainGestureColor,eventsInterceptionEnabled,gestureColor,orientation"
+ render="skip" />
+ <view
+ class="android.view.TextureView"
+ render="skip" />
+ <view
+ class="android.view.SurfaceView"
+ render="skip" />
+ <view
+ class="android.widget.NumberPicker"
+ topAttrs="focusable,focusableInTouchMode"
+ relatedTo="TimePicker,DatePicker"
+ render="alone" />
+ <view
+ class="android.widget.ZoomButton"
+ topAttrs="background"
+ relatedTo="Button,ZoomControls" />
+ <view
+ class="android.widget.ZoomControls"
+ topAttrs="style,background,gravity"
+ relatedTo="ZoomButton"
+ resize="none" />
+ <view
+ class="merge"
+ topAttrs="orientation,gravity,style"
+ skip="true"
+ render="skip" />
+ <view
+ class="android.widget.DialerFilter"
+ fill="width_in_vertical"
+ render="skip" />
+ <view
+ class="android.widget.TwoLineListItem"
+ topAttrs="mode,paddingBottom,paddingTop,minHeight,paddingLeft"
+ render="skip" />
+ <view
+ class="android.widget.AbsoluteLayout"
+ topAttrs="background,orientation,paddingBottom,paddingLeft,paddingRight,paddingTop"
+ name="AbsoluteLayout (Deprecated)"
+ fill="opposite"
+ render="skip" />
+ </category>
+ <category
+ name="Other">
+ <!-- This is the catch-all category which contains unknown views if we encounter any -->
+ </category>
+ <!-- TODO: Add-ons? -->
+</metadata>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml
new file mode 100644
index 000000000..96c7fe7d2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml
@@ -0,0 +1,382 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Default configuration for various views to be rendered
+ TODO: Remove views that don't have custom configuration
+ TODO: Parameterize the custom width (200dip) in the below?
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <AnalogClock
+ android:layout_width="wrap_content"
+ android:id="@+id/android_widget_AnalogClock"
+ android:layout_height="75dip">
+ </AnalogClock>
+ <AutoCompleteTextView
+ android:layout_height="wrap_content"
+ android:layout_width="200dip"
+ android:text="AutoComplete"
+ android:id="@+id/android_widget_AutoCompleteTextView">
+ </AutoCompleteTextView>
+ <Button
+ android:text="Button"
+ android:id="@+id/android_widget_Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </Button>
+ <Button
+ android:text="Small"
+ style="?android:attr/buttonStyleSmall"
+ android:id="@+id/android_widget_SmallButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </Button>
+ <CheckBox
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="CheckBox"
+ android:id="@+id/android_widget_CheckBox"
+ android:checked="true">
+ </CheckBox>
+ <CheckedTextView
+ android:text="CheckedTextView"
+ android:id="@+id/android_widget_CheckedTextView"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content">
+ </CheckedTextView>
+ <!--
+ <Chronometer
+ android:text="Chronometer"
+ android:id="@+id/android_widget_Chronometer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </Chronometer>
+ -->
+ <DigitalClock
+ android:text="DigitalClock"
+ android:id="@+id/android_widget_DigitalClock"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </DigitalClock>
+
+ <EditText
+ android:id="@+id/PlainText"
+ android:text="abc"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/Password"
+ android:inputType="textPassword"
+ android:text="••••••••"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <!-- android:inputType="numberPassword" not used here to allow digits in preview only -->
+ <EditText
+ android:id="@+id/PasswordNumeric"
+ android:text="1•••2•••3"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/PersonName"
+ android:inputType="textPersonName"
+ android:text="Firstname Lastname"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/Phone"
+ android:inputType="phone"
+ android:text="(555) 0100"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/PostalAddress"
+ android:inputType="textPostalAddress"
+ android:text="Address"
+ android:layout_width="200dip"
+ android:layout_height="100dip">
+ </EditText>
+
+ <EditText
+ android:id="@+id/MultilineText"
+ android:inputType="textMultiLine"
+ android:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor"
+ android:layout_width="200dip"
+ android:layout_height="100dip">
+ </EditText>
+
+ <EditText
+ android:id="@+id/Date"
+ android:inputType="date"
+ android:text="1/1/2011"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/Time"
+ android:inputType="time"
+ android:text="12:00am"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/Email"
+ android:inputType="textEmailAddress"
+ android:text="user@domain"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/Number"
+ android:inputType="number"
+ android:text="42"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/NumberSigned"
+ android:inputType="numberSigned"
+ android:text="-42"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <EditText
+ android:id="@+id/NumberDecimal"
+ android:inputType="numberDecimal"
+ android:text="42.0"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content">
+ </EditText>
+
+ <TextView
+ android:text="Large"
+ android:id="@+id/LargeText"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </TextView>
+
+ <TextView
+ android:text="Medium"
+ android:id="@+id/MediumText"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </TextView>
+
+ <TextView
+ android:text="Small"
+ android:id="@+id/SmallText"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </TextView>
+
+ <MultiAutoCompleteTextView
+ android:layout_height="wrap_content"
+ android:layout_width="200dip"
+ android:text="MultiAutoComplete"
+ android:id="@+id/android_widget_MultiAutoCompleteTextView">
+ </MultiAutoCompleteTextView>
+ <ProgressBar
+ android:id="@+id/android_widget_ProgressBarNormal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </ProgressBar>
+ <ProgressBar
+ android:id="@+id/android_widget_ProgressBarHorizontal"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content"
+ android:progress="30"
+ style="?android:attr/progressBarStyleHorizontal">
+ </ProgressBar>
+ <ProgressBar
+ android:id="@+id/android_widget_ProgressBarLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:attr/progressBarStyleLarge">
+ </ProgressBar>
+ <ProgressBar
+ android:id="@+id/android_widget_ProgressBarSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:attr/progressBarStyleSmall">
+ </ProgressBar>
+ <QuickContactBadge
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:id="@+id/android_widget_QuickContactBadge">
+ </QuickContactBadge>
+ <RadioButton
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:id="@+id/android_widget_RadioButton"
+ android:text="RadioButton"
+ android:checked="true">
+ </RadioButton>
+ <RatingBar
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:id="@+id/android_widget_RatingBar"
+ android:rating="1">
+ </RatingBar>
+ <SeekBar
+ android:layout_height="wrap_content"
+ android:id="@+id/android_widget_SeekBar"
+ android:layout_width="200dip"
+ android:progress="30">
+ </SeekBar>
+ <ListView
+ android:id="@+id/android_widget_ListView"
+ android:layout_width="200dip"
+ android:layout_height="60dip"
+ android:divider="#333333"
+ android:dividerHeight="1px"
+ >
+ </ListView>
+ <ExpandableListView
+ android:id="@+id/android_widget_ExpandableListView"
+ android:layout_width="200dip"
+ android:layout_height="60dip"
+ android:divider="#333333"
+ android:dividerHeight="1px"
+ >
+ </ExpandableListView>
+ <Spinner
+ android:layout_height="wrap_content"
+ android:id="@+id/android_widget_Spinner"
+ android:layout_width="200dip">
+ </Spinner>
+ <TextView
+ android:text="TextView"
+ android:id="@+id/android_widget_TextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </TextView>
+ <ToggleButton
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:checked="false"
+ android:id="@+id/android_widget_ToggleButton"
+ android:text="ToggleButton">
+ </ToggleButton>
+ <ZoomButton
+ android:id="@+id/android_widget_ZoomButton"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:src="@android:drawable/btn_plus">
+ </ZoomButton>
+ <ZoomControls
+ android:id="@+id/android_widget_ZoomControls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </ZoomControls>
+ <Switch
+ android:id="@+id/android_widget_Switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TimePicker
+ android:id="@+id/android_widget_TimePicker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </TimePicker>
+ <DatePicker
+ android:id="@+id/android_widget_DatePicker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ </DatePicker>
+ <CalendarView
+ android:id="@+id/android_widget_CalendarView"
+ android:layout_width="200dip"
+ android:layout_height="200dip">
+ </CalendarView>
+ <RadioGroup
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:orientation="horizontal"
+ android:id="@+id/android_widget_RadioGroup">
+ <RadioButton
+ android:checked="true">
+ </RadioButton>
+ <RadioButton></RadioButton>
+ <RadioButton></RadioButton>
+ </RadioGroup>
+ <TabHost
+ android:id="@android:id/tabhost"
+ android:layout_width="200dip"
+ android:layout_height="100dip">
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <TabWidget
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@android:id/tabs">
+ </TabWidget>
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@android:id/tabcontent">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/Tab1">
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/Tab2">
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/Tab3">
+ </LinearLayout>
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+ <TabHost
+ android:id="@android:id/tabhost"
+ android:layout_width="70dip"
+ android:layout_height="100dip">
+ <LinearLayout
+ android:id="@+id/android_widget_TabWidget"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <TabWidget
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@android:id/tabs">
+ </TabWidget>
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@android:id/tabcontent">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/Tab1">
+ </LinearLayout>
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+</LinearLayout>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java
new file mode 100644
index 000000000..d6ff4d51d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.wb.internal.core.DesignerPlugin;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
+import org.eclipse.wb.internal.core.utils.ui.DrawUtils;
+
+/**
+ * Handle an XML property which represents booleans.
+ *
+ * Similar to the WindowBuilder PropertyEditor, but operates on Strings rather
+ * than Booleans (which means it is a tri-state boolean: true, false, not set)
+ */
+public class BooleanXmlPropertyEditor extends XmlPropertyEditor {
+ public static final BooleanXmlPropertyEditor INSTANCE = new BooleanXmlPropertyEditor();
+
+ private static final Image mTrueImage = DesignerPlugin.getImage("properties/true.png");
+ private static final Image mFalseImage = DesignerPlugin.getImage("properties/false.png");
+ private static final Image mNullImage =
+ DesignerPlugin.getImage("properties/BooleanNull.png");
+ private static final Image mUnknownImage =
+ DesignerPlugin.getImage("properties/BooleanUnknown.png");
+
+ private BooleanXmlPropertyEditor() {
+ }
+
+ @Override
+ public void paint(Property property, GC gc, int x, int y, int width, int height)
+ throws Exception {
+ Object value = property.getValue();
+ assert value == null || value instanceof String;
+ if (value == null || value instanceof String) {
+ String text = (String) value;
+ Image image;
+ if (VALUE_TRUE.equals(text)) {
+ image = mTrueImage;
+ } else if (VALUE_FALSE.equals(text)) {
+ image = mFalseImage;
+ } else if (text == null) {
+ image = mNullImage;
+ } else {
+ // Probably something like a reference, e.g. @boolean/foo
+ image = mUnknownImage;
+ }
+
+ // draw image
+ DrawUtils.drawImageCV(gc, image, x, y, height);
+
+ // prepare new position/width
+ int imageWidth = image.getBounds().width + 2;
+ width -= imageWidth;
+
+ // draw text
+ if (text != null) {
+ x += imageWidth;
+ DrawUtils.drawStringCV(gc, text, x, y, width, height);
+ }
+ }
+ }
+
+ @Override
+ public boolean activate(PropertyTable propertyTable, Property property, Point location)
+ throws Exception {
+ // check that user clicked on image
+ if (location == null || location.x < mTrueImage.getBounds().width + 2) {
+ cycleValue(property);
+ }
+ // don't activate
+ return false;
+ }
+
+ @Override
+ public void doubleClick(Property property, Point location) throws Exception {
+ cycleValue(property);
+ }
+
+ /**
+ * Cycles through the values
+ */
+ private void cycleValue(Property property) throws Exception {
+ Object value = property.getValue();
+ if (value == null || value instanceof String) {
+ // Cycle null => true => false => null
+ String text = (String) value;
+ if (VALUE_TRUE.equals(text)) {
+ property.setValue(VALUE_FALSE);
+ } else if (VALUE_FALSE.equals(text)) {
+ property.setValue(null);
+ } else {
+ property.setValue(VALUE_TRUE);
+ }
+ } else {
+ assert false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java
new file mode 100644
index 000000000..f1a3f2aaa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor;
+
+import org.eclipse.wb.core.controls.CCombo3;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.editor.AbstractComboPropertyEditor;
+import org.eclipse.wb.internal.core.model.property.editor.ITextValuePropertyEditor;
+
+class EnumXmlPropertyEditor extends AbstractComboPropertyEditor implements
+ ITextValuePropertyEditor {
+ public static final EnumXmlPropertyEditor INSTANCE = new EnumXmlPropertyEditor();
+
+ private EnumXmlPropertyEditor() {
+ }
+
+ @Override
+ protected String getText(Property property) throws Exception {
+ Object value = property.getValue();
+ if (value == null) {
+ return "";
+ } else if (value instanceof String) {
+ return (String) value;
+ } else if (value == Property.UNKNOWN_VALUE) {
+ return "<varies>";
+ } else {
+ return "";
+ }
+ }
+
+ private String[] getItems(Property property) {
+ XmlProperty xmlProperty = (XmlProperty) property;
+ AttributeDescriptor descriptor = xmlProperty.getDescriptor();
+ assert descriptor instanceof ListAttributeDescriptor;
+ ListAttributeDescriptor list = (ListAttributeDescriptor) descriptor;
+ return list.getValues();
+ }
+
+ @Override
+ protected void addItems(Property property, CCombo3 combo) throws Exception {
+ for (String item : getItems(property)) {
+ combo.add(item);
+ }
+ }
+
+ @Override
+ protected void selectItem(Property property, CCombo3 combo) throws Exception {
+ combo.setText(getText(property));
+ }
+
+ @Override
+ protected void toPropertyEx(Property property, CCombo3 combo, int index) throws Exception {
+ property.setValue(getItems(property)[index]);
+ }
+
+ @Override
+ public void setText(Property property, String text) throws Exception {
+ property.setValue(text);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java
new file mode 100644
index 000000000..5e1e7029f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.google.common.base.Splitter;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.wb.internal.core.utils.execution.ExecutionUtils;
+import org.eclipse.wb.internal.core.utils.execution.RunnableEx;
+import org.eclipse.wb.internal.core.utils.ui.dialogs.ResizableDialog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class FlagXmlPropertyDialog extends ResizableDialog
+implements IStructuredContentProvider, ICheckStateListener, SelectionListener, KeyListener {
+ private final String mTitle;
+ private final XmlProperty mProperty;
+ private final String[] mFlags;
+ private final boolean mIsRadio;
+
+ private Table mTable;
+ private CheckboxTableViewer mViewer;
+
+ FlagXmlPropertyDialog(
+ @NonNull Shell parentShell,
+ @NonNull String title,
+ boolean isRadio,
+ @NonNull String[] flags,
+ @NonNull XmlProperty property) {
+ super(parentShell, AdtPlugin.getDefault());
+ mTitle = title;
+ mIsRadio = isRadio;
+ mFlags = flags;
+ mProperty = property;
+ }
+
+ @Override
+ protected void configureShell(Shell newShell) {
+ super.configureShell(newShell);
+ newShell.setText(mTitle);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite container = (Composite) super.createDialogArea(parent);
+
+ mViewer = CheckboxTableViewer.newCheckList(container,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.HIDE_SELECTION);
+ mTable = mViewer.getTable();
+ mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+ Composite workaround = PropertyFactory.addWorkaround(container);
+ if (workaround != null) {
+ workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
+ }
+
+ mViewer.setContentProvider(this);
+ mViewer.setInput(mFlags);
+
+ String current = mProperty.getStringValue();
+ if (current != null) {
+ Object[] checked = null;
+ if (mIsRadio) {
+ checked = new String[] { current };
+ } else {
+ List<String> flags = new ArrayList<String>();
+ for (String s : Splitter.on('|').omitEmptyStrings().trimResults().split(current)) {
+ flags.add(s);
+ }
+ checked = flags.toArray(new String[flags.size()]);
+ }
+ mViewer.setCheckedElements(checked);
+ }
+ if (mFlags.length > 0) {
+ mTable.setSelection(0);
+ }
+
+ if (mIsRadio) {
+ // Enforce single-item selection
+ mViewer.addCheckStateListener(this);
+ }
+ mTable.addSelectionListener(this);
+ mTable.addKeyListener(this);
+
+ return container;
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+ createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+ }
+
+ @Override
+ protected Point getDefaultSize() {
+ return new Point(450, 400);
+ }
+
+ @Override
+ protected void okPressed() {
+ // Apply the value
+ ExecutionUtils.runLog(new RunnableEx() {
+ @Override
+ public void run() throws Exception {
+ StringBuilder sb = new StringBuilder(30);
+ for (Object o : mViewer.getCheckedElements()) {
+ if (sb.length() > 0) {
+ sb.append('|');
+ }
+ sb.append((String) o);
+ }
+ String value = sb.length() > 0 ? sb.toString() : null;
+ mProperty.setValue(value);
+ }
+ });
+
+ // close dialog
+ super.okPressed();
+ }
+
+ // ---- Implements IStructuredContentProvider ----
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return (Object []) inputElement;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ // ---- Implements ICheckStateListener ----
+
+ @Override
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ // Try to disable other elements that conflict with this
+ boolean isChecked = event.getChecked();
+ if (isChecked) {
+ Object selected = event.getElement();
+ for (Object other : mViewer.getCheckedElements()) {
+ if (other != selected) {
+ mViewer.setChecked(other, false);
+ }
+ }
+ } else {
+
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ if (e.item instanceof TableItem) {
+ TableItem item = (TableItem) e.item;
+ item.setChecked(!item.getChecked());
+ }
+ }
+
+ // ---- Implements KeyListener ----
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ // Let space toggle checked state
+ if (e.keyCode == ' ' /* SWT.SPACE requires Eclipse 3.7 */) {
+ if (mTable.getSelectionCount() == 1) {
+ TableItem item = mTable.getSelection()[0];
+ item.setChecked(!item.getChecked());
+ }
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java
new file mode 100644
index 000000000..2b8cfbf43
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java
@@ -0,0 +1,750 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.browser.IWebBrowser;
+import org.eclipse.wb.internal.core.editor.structure.property.PropertyListIntersector;
+import org.eclipse.wb.internal.core.model.property.ComplexProperty;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.category.PropertyCategory;
+import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor;
+import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * The {@link PropertyFactory} creates (and caches) the set of {@link Property}
+ * instances applicable to a given node. It's also responsible for ordering
+ * these, and sometimes combining them into {@link ComplexProperty} category
+ * nodes.
+ * <p>
+ * TODO: For any properties that are *set* in XML, they should NOT be labeled as
+ * advanced (which would make them disappear)
+ */
+public class PropertyFactory {
+ /** Disable cache during development only */
+ @SuppressWarnings("unused")
+ private static final boolean CACHE_ENABLED = true || !LintUtils.assertionsEnabled();
+ static {
+ if (!CACHE_ENABLED) {
+ System.err.println("WARNING: The property cache is disabled");
+ }
+ }
+
+ private static final Property[] NO_PROPERTIES = new Property[0];
+
+ private static final int PRIO_FIRST = -100000;
+ private static final int PRIO_SECOND = PRIO_FIRST + 10;
+ private static final int PRIO_LAST = 100000;
+
+ private final GraphicalEditorPart mGraphicalEditorPart;
+ private Map<UiViewElementNode, Property[]> mCache =
+ new WeakHashMap<UiViewElementNode, Property[]>();
+ private UiViewElementNode mCurrentViewCookie;
+
+ /** Sorting orders for the properties */
+ public enum SortingMode {
+ NATURAL,
+ BY_ORIGIN,
+ ALPHABETICAL;
+ }
+
+ /** The default sorting mode */
+ public static final SortingMode DEFAULT_MODE = SortingMode.BY_ORIGIN;
+
+ private SortingMode mSortMode = DEFAULT_MODE;
+ private SortingMode mCacheSortMode;
+
+ public PropertyFactory(GraphicalEditorPart graphicalEditorPart) {
+ mGraphicalEditorPart = graphicalEditorPart;
+ }
+
+ /**
+ * Get the properties for the given list of selection items.
+ *
+ * @param items the {@link CanvasViewInfo} instances to get an intersected
+ * property list for
+ * @return the properties for the given items
+ */
+ public Property[] getProperties(List<CanvasViewInfo> items) {
+ mCurrentViewCookie = null;
+
+ if (items == null || items.size() == 0) {
+ return NO_PROPERTIES;
+ } else if (items.size() == 1) {
+ CanvasViewInfo item = items.get(0);
+ mCurrentViewCookie = item.getUiViewNode();
+
+ return getProperties(item);
+ } else {
+ // intersect properties
+ PropertyListIntersector intersector = new PropertyListIntersector();
+ for (CanvasViewInfo node : items) {
+ intersector.intersect(getProperties(node));
+ }
+
+ return intersector.getProperties();
+ }
+ }
+
+ private Property[] getProperties(CanvasViewInfo item) {
+ UiViewElementNode node = item.getUiViewNode();
+ if (node == null) {
+ return NO_PROPERTIES;
+ }
+
+ if (mCacheSortMode != mSortMode) {
+ mCacheSortMode = mSortMode;
+ mCache.clear();
+ }
+
+ Property[] properties = mCache.get(node);
+ if (!CACHE_ENABLED) {
+ properties = null;
+ }
+ if (properties == null) {
+ Collection<? extends Property> propertyList = getProperties(node);
+ if (propertyList == null) {
+ properties = new Property[0];
+ } else {
+ properties = propertyList.toArray(new Property[propertyList.size()]);
+ }
+ mCache.put(node, properties);
+ }
+ return properties;
+ }
+
+
+ protected Collection<? extends Property> getProperties(UiViewElementNode node) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) node.getDescriptor();
+ String fqcn = viewDescriptor.getFullClassName();
+ Set<String> top = new HashSet<String>(repository.getTopAttributes(fqcn));
+ AttributeDescriptor[] attributeDescriptors = node.getAttributeDescriptors();
+
+ List<XmlProperty> properties = new ArrayList<XmlProperty>(attributeDescriptors.length);
+ int priority = 0;
+ for (final AttributeDescriptor descriptor : attributeDescriptors) {
+ // TODO: Filter out non-public properties!!
+ // (They shouldn't be in the descriptors at all)
+
+ assert !(descriptor instanceof SeparatorAttributeDescriptor); // No longer inserted
+ if (descriptor instanceof XmlnsAttributeDescriptor) {
+ continue;
+ }
+
+ PropertyEditor editor = XmlPropertyEditor.INSTANCE;
+ IAttributeInfo info = descriptor.getAttributeInfo();
+ if (info != null) {
+ EnumSet<Format> formats = info.getFormats();
+ if (formats.contains(Format.BOOLEAN)) {
+ editor = BooleanXmlPropertyEditor.INSTANCE;
+ } else if (formats.contains(Format.ENUM)) {
+ // We deliberately don't use EnumXmlPropertyEditor.INSTANCE here,
+ // since some attributes (such as layout_width) can have not just one
+ // of the enum values but custom values such as "42dp" as well. And
+ // furthermore, we don't even bother limiting this to formats.size()==1,
+ // since the editing experience with the enum property editor is
+ // more limited than the text editor plus enum completer anyway
+ // (for example, you can't type to filter the values, and clearing
+ // the value is harder.)
+ }
+ }
+
+ XmlProperty property = new XmlProperty(editor, this, node, descriptor);
+ // Assign ids sequentially. This ensures that the properties will mostly keep their
+ // relative order (such as placing width before height), even though we will regroup
+ // some (such as properties in the same category, and the layout params etc)
+ priority += 10;
+
+ PropertyCategory category = PropertyCategory.NORMAL;
+ String name = descriptor.getXmlLocalName();
+ if (top.contains(name) || PropertyMetadata.isPreferred(name)) {
+ category = PropertyCategory.PREFERRED;
+ property.setPriority(PRIO_FIRST + priority);
+ } else {
+ property.setPriority(priority);
+
+ // Prefer attributes defined on the specific type of this
+ // widget
+ // NOTE: This doesn't work very well for TextViews
+ /* IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
+ if (attributeInfo != null && fqcn.equals(attributeInfo.getDefinedBy())) {
+ category = PropertyCategory.PREFERRED;
+ } else*/ if (PropertyMetadata.isAdvanced(name)) {
+ category = PropertyCategory.ADVANCED;
+ }
+ }
+ if (category != null) {
+ property.setCategory(category);
+ }
+ properties.add(property);
+ }
+
+ switch (mSortMode) {
+ case BY_ORIGIN:
+ return sortByOrigin(node, properties);
+
+ case ALPHABETICAL:
+ return sortAlphabetically(node, properties);
+
+ default:
+ case NATURAL:
+ return sortNatural(node, properties);
+ }
+ }
+
+ protected Collection<? extends Property> sortAlphabetically(
+ UiViewElementNode node,
+ List<XmlProperty> properties) {
+ Collections.sort(properties, Property.ALPHABETICAL);
+ return properties;
+ }
+
+ protected Collection<? extends Property> sortByOrigin(
+ UiViewElementNode node,
+ List<XmlProperty> properties) {
+ List<Property> collapsed = new ArrayList<Property>(properties.size());
+ List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20);
+ List<Property> marginProperties = null;
+ List<Property> deprecatedProperties = null;
+ Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>();
+ Multimap<String, Property> categoryToProperties = ArrayListMultimap.create();
+
+ if (properties.isEmpty()) {
+ return properties;
+ }
+
+ ViewElementDescriptor parent = (ViewElementDescriptor) properties.get(0).getDescriptor()
+ .getParent();
+ Map<String, Integer> categoryPriorities = Maps.newHashMap();
+ int nextCategoryPriority = 100;
+ while (parent != null) {
+ categoryPriorities.put(parent.getFullClassName(), nextCategoryPriority += 100);
+ parent = parent.getSuperClassDesc();
+ }
+
+ for (int i = 0, max = properties.size(); i < max; i++) {
+ XmlProperty property = properties.get(i);
+
+ AttributeDescriptor descriptor = property.getDescriptor();
+ if (descriptor.isDeprecated()) {
+ if (deprecatedProperties == null) {
+ deprecatedProperties = Lists.newArrayListWithExpectedSize(10);
+ }
+ deprecatedProperties.add(property);
+ continue;
+ }
+
+ String firstName = descriptor.getXmlLocalName();
+ if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) {
+ if (marginProperties == null) {
+ marginProperties = Lists.newArrayListWithExpectedSize(5);
+ }
+ marginProperties.add(property);
+ } else {
+ layoutProperties.add(property);
+ }
+ continue;
+ }
+
+ if (firstName.equals(ATTR_ID)) {
+ // Add id to the front (though the layout parameters will be added to
+ // the front of this at the end)
+ property.setPriority(PRIO_FIRST);
+ collapsed.add(property);
+ continue;
+ }
+
+ if (property.getCategory() == PropertyCategory.PREFERRED) {
+ collapsed.add(property);
+ // Fall through: these are *duplicated* inside their defining categories!
+ // However, create a new instance of the property, such that the propertysheet
+ // doesn't see the same property instance twice (when selected, it will highlight
+ // both, etc.) Also, set the category to Normal such that we don't draw attention
+ // to it again. We want it to appear in both places such that somebody looking
+ // within a category will always find it there, even if for this specific
+ // view type it's a common attribute and replicated up at the top.
+ XmlProperty oldProperty = property;
+ property = new XmlProperty(oldProperty.getEditor(), this, node,
+ oldProperty.getDescriptor());
+ property.setPriority(oldProperty.getPriority());
+ }
+
+ IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
+ if (attributeInfo != null && attributeInfo.getDefinedBy() != null) {
+ String category = attributeInfo.getDefinedBy();
+ ComplexProperty complex = categoryToProperty.get(category);
+ if (complex == null) {
+ complex = new ComplexProperty(
+ category.substring(category.lastIndexOf('.') + 1),
+ "[]",
+ null /* properties */);
+ categoryToProperty.put(category, complex);
+ Integer categoryPriority = categoryPriorities.get(category);
+ if (categoryPriority != null) {
+ complex.setPriority(categoryPriority);
+ } else {
+ // Descriptor for an attribute whose definedBy does *not*
+ // correspond to one of the known superclasses of this widget.
+ // This sometimes happens; for example, a RatingBar will pull in
+ // an ImageView's minWidth attribute. Probably an error in the
+ // metadata, but deal with it gracefully here.
+ categoryPriorities.put(category, nextCategoryPriority += 100);
+ complex.setPriority(nextCategoryPriority);
+ }
+ }
+ categoryToProperties.put(category, property);
+ continue;
+ } else {
+ collapsed.add(property);
+ }
+ }
+
+ // Update the complex properties
+ for (String category : categoryToProperties.keySet()) {
+ Collection<Property> subProperties = categoryToProperties.get(category);
+ if (subProperties.size() > 1) {
+ ComplexProperty complex = categoryToProperty.get(category);
+ assert complex != null : category;
+ Property[] subArray = new Property[subProperties.size()];
+ complex.setProperties(subProperties.toArray(subArray));
+ //complex.setPriority(subArray[0].getPriority());
+
+ collapsed.add(complex);
+
+ boolean allAdvanced = true;
+ boolean isPreferred = false;
+ for (Property p : subProperties) {
+ PropertyCategory c = p.getCategory();
+ if (c != PropertyCategory.ADVANCED) {
+ allAdvanced = false;
+ }
+ if (c == PropertyCategory.PREFERRED) {
+ isPreferred = true;
+ }
+ }
+ if (isPreferred) {
+ complex.setCategory(PropertyCategory.PREFERRED);
+ } else if (allAdvanced) {
+ complex.setCategory(PropertyCategory.ADVANCED);
+ }
+ } else if (subProperties.size() == 1) {
+ collapsed.add(subProperties.iterator().next());
+ }
+ }
+
+ if (layoutProperties.size() > 0 || marginProperties != null) {
+ if (marginProperties != null) {
+ XmlProperty[] m =
+ marginProperties.toArray(new XmlProperty[marginProperties.size()]);
+ Property marginProperty = new ComplexProperty(
+ "Margins",
+ "[]",
+ m);
+ layoutProperties.add(marginProperty);
+ marginProperty.setPriority(PRIO_LAST);
+
+ for (XmlProperty p : m) {
+ p.setParent(marginProperty);
+ }
+ }
+ Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]);
+ Arrays.sort(l, Property.PRIORITY);
+ Property property = new ComplexProperty(
+ "Layout Parameters",
+ "[]",
+ l);
+ for (Property p : l) {
+ if (p instanceof XmlProperty) {
+ ((XmlProperty) p).setParent(property);
+ }
+ }
+ property.setCategory(PropertyCategory.PREFERRED);
+ collapsed.add(property);
+ property.setPriority(PRIO_SECOND);
+ }
+
+ if (deprecatedProperties != null && deprecatedProperties.size() > 0) {
+ Property property = new ComplexProperty(
+ "Deprecated",
+ "(Deprecated Properties)",
+ deprecatedProperties.toArray(new Property[deprecatedProperties.size()]));
+ property.setPriority(PRIO_LAST);
+ collapsed.add(property);
+ }
+
+ Collections.sort(collapsed, Property.PRIORITY);
+
+ return collapsed;
+ }
+
+ protected Collection<? extends Property> sortNatural(
+ UiViewElementNode node,
+ List<XmlProperty> properties) {
+ Collections.sort(properties, Property.ALPHABETICAL);
+ List<Property> collapsed = new ArrayList<Property>(properties.size());
+ List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20);
+ List<Property> marginProperties = null;
+ List<Property> deprecatedProperties = null;
+ Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>();
+ Multimap<String, Property> categoryToProperties = ArrayListMultimap.create();
+
+ for (int i = 0, max = properties.size(); i < max; i++) {
+ XmlProperty property = properties.get(i);
+
+ AttributeDescriptor descriptor = property.getDescriptor();
+ if (descriptor.isDeprecated()) {
+ if (deprecatedProperties == null) {
+ deprecatedProperties = Lists.newArrayListWithExpectedSize(10);
+ }
+ deprecatedProperties.add(property);
+ continue;
+ }
+
+ String firstName = descriptor.getXmlLocalName();
+ if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) {
+ if (marginProperties == null) {
+ marginProperties = Lists.newArrayListWithExpectedSize(5);
+ }
+ marginProperties.add(property);
+ } else {
+ layoutProperties.add(property);
+ }
+ continue;
+ }
+
+ if (firstName.equals(ATTR_ID)) {
+ // Add id to the front (though the layout parameters will be added to
+ // the front of this at the end)
+ property.setPriority(PRIO_FIRST);
+ collapsed.add(property);
+ continue;
+ }
+
+ String category = PropertyMetadata.getCategory(firstName);
+ if (category != null) {
+ ComplexProperty complex = categoryToProperty.get(category);
+ if (complex == null) {
+ complex = new ComplexProperty(
+ category,
+ "[]",
+ null /* properties */);
+ categoryToProperty.put(category, complex);
+ complex.setPriority(property.getPriority());
+ }
+ categoryToProperties.put(category, property);
+ continue;
+ }
+
+ // Index of second word in the first name, so in fooBar it's 3 (index of 'B')
+ int firstNameIndex = firstName.length();
+ for (int k = 0, kn = firstName.length(); k < kn; k++) {
+ if (Character.isUpperCase(firstName.charAt(k))) {
+ firstNameIndex = k;
+ break;
+ }
+ }
+
+ // Scout forwards and see how many properties we can combine
+ int j = i + 1;
+ if (property.getCategory() != PropertyCategory.PREFERRED
+ && !property.getDescriptor().isDeprecated()) {
+ for (; j < max; j++) {
+ XmlProperty next = properties.get(j);
+ String nextName = next.getName();
+ if (nextName.regionMatches(0, firstName, 0, firstNameIndex)
+ // Also make sure we begin the second word at the next
+ // character; if not, we could have something like
+ // scrollBar
+ // scrollingBehavior
+ && nextName.length() > firstNameIndex
+ && Character.isUpperCase(nextName.charAt(firstNameIndex))) {
+
+ // Deprecated attributes, and preferred attributes, should not
+ // be pushed into normal clusters (preferred stay top-level
+ // and sort to the top, deprecated are all put in the same cluster at
+ // the end)
+
+ if (next.getCategory() == PropertyCategory.PREFERRED) {
+ break;
+ }
+ if (next.getDescriptor().isDeprecated()) {
+ break;
+ }
+
+ // This property should be combined with the previous
+ // property
+ } else {
+ break;
+ }
+ }
+ }
+ if (j - i > 1) {
+ // Combining multiple properties: all the properties from i
+ // through j inclusive
+ XmlProperty[] subprops = new XmlProperty[j - i];
+ for (int k = i, index = 0; k < j; k++, index++) {
+ subprops[index] = properties.get(k);
+ }
+ Arrays.sort(subprops, Property.PRIORITY);
+
+ // See if we can compute a LONGER base than just the first word.
+ // For example, if we have "lineSpacingExtra" and "lineSpacingMultiplier"
+ // we'd like the base to be "lineSpacing", not "line".
+ int common = firstNameIndex;
+ for (int k = firstNameIndex + 1, n = firstName.length(); k < n; k++) {
+ if (Character.isUpperCase(firstName.charAt(k))) {
+ common = k;
+ break;
+ }
+ }
+ if (common > firstNameIndex) {
+ for (int k = 0, n = subprops.length; k < n; k++) {
+ String nextName = subprops[k].getName();
+ if (nextName.regionMatches(0, firstName, 0, common)
+ // Also make sure we begin the second word at the next
+ // character; if not, we could have something like
+ // scrollBar
+ // scrollingBehavior
+ && nextName.length() > common
+ && Character.isUpperCase(nextName.charAt(common))) {
+ // New prefix is okay
+ } else {
+ common = firstNameIndex;
+ break;
+ }
+ }
+ firstNameIndex = common;
+ }
+
+ String base = firstName.substring(0, firstNameIndex);
+ base = DescriptorsUtils.capitalize(base);
+ Property complexProperty = new ComplexProperty(
+ base,
+ "[]",
+ subprops);
+ complexProperty.setPriority(subprops[0].getPriority());
+ //complexProperty.setCategory(PropertyCategory.PREFERRED);
+ collapsed.add(complexProperty);
+ boolean allAdvanced = true;
+ boolean isPreferred = false;
+ for (XmlProperty p : subprops) {
+ p.setParent(complexProperty);
+ PropertyCategory c = p.getCategory();
+ if (c != PropertyCategory.ADVANCED) {
+ allAdvanced = false;
+ }
+ if (c == PropertyCategory.PREFERRED) {
+ isPreferred = true;
+ }
+ }
+ if (isPreferred) {
+ complexProperty.setCategory(PropertyCategory.PREFERRED);
+ } else if (allAdvanced) {
+ complexProperty.setCategory(PropertyCategory.PREFERRED);
+ }
+ } else {
+ // Add the individual properties (usually 1, sometimes 2
+ for (int k = i; k < j; k++) {
+ collapsed.add(properties.get(k));
+ }
+ }
+
+ i = j - 1; // -1: compensate in advance for the for-loop adding 1
+ }
+
+ // Update the complex properties
+ for (String category : categoryToProperties.keySet()) {
+ Collection<Property> subProperties = categoryToProperties.get(category);
+ if (subProperties.size() > 1) {
+ ComplexProperty complex = categoryToProperty.get(category);
+ assert complex != null : category;
+ Property[] subArray = new Property[subProperties.size()];
+ complex.setProperties(subProperties.toArray(subArray));
+ complex.setPriority(subArray[0].getPriority());
+ collapsed.add(complex);
+
+ boolean allAdvanced = true;
+ boolean isPreferred = false;
+ for (Property p : subProperties) {
+ PropertyCategory c = p.getCategory();
+ if (c != PropertyCategory.ADVANCED) {
+ allAdvanced = false;
+ }
+ if (c == PropertyCategory.PREFERRED) {
+ isPreferred = true;
+ }
+ }
+ if (isPreferred) {
+ complex.setCategory(PropertyCategory.PREFERRED);
+ } else if (allAdvanced) {
+ complex.setCategory(PropertyCategory.ADVANCED);
+ }
+ } else if (subProperties.size() == 1) {
+ collapsed.add(subProperties.iterator().next());
+ }
+ }
+
+ if (layoutProperties.size() > 0 || marginProperties != null) {
+ if (marginProperties != null) {
+ XmlProperty[] m =
+ marginProperties.toArray(new XmlProperty[marginProperties.size()]);
+ Property marginProperty = new ComplexProperty(
+ "Margins",
+ "[]",
+ m);
+ layoutProperties.add(marginProperty);
+ marginProperty.setPriority(PRIO_LAST);
+
+ for (XmlProperty p : m) {
+ p.setParent(marginProperty);
+ }
+ }
+ Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]);
+ Arrays.sort(l, Property.PRIORITY);
+ Property property = new ComplexProperty(
+ "Layout Parameters",
+ "[]",
+ l);
+ for (Property p : l) {
+ if (p instanceof XmlProperty) {
+ ((XmlProperty) p).setParent(property);
+ }
+ }
+ property.setCategory(PropertyCategory.PREFERRED);
+ collapsed.add(property);
+ property.setPriority(PRIO_SECOND);
+ }
+
+ if (deprecatedProperties != null && deprecatedProperties.size() > 0) {
+ Property property = new ComplexProperty(
+ "Deprecated",
+ "(Deprecated Properties)",
+ deprecatedProperties.toArray(new Property[deprecatedProperties.size()]));
+ property.setPriority(PRIO_LAST);
+ collapsed.add(property);
+ }
+
+ Collections.sort(collapsed, Property.PRIORITY);
+
+ return collapsed;
+ }
+
+ @Nullable
+ GraphicalEditorPart getGraphicalEditor() {
+ return mGraphicalEditorPart;
+ }
+
+ // HACK: This should be passed into each property instead
+ public Object getCurrentViewObject() {
+ return mCurrentViewCookie;
+ }
+
+ public void setSortingMode(SortingMode sortingMode) {
+ mSortMode = sortingMode;
+ }
+
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574
+ public static Composite addWorkaround(Composite parent) {
+ if (ButtonPropertyEditorPresentation.isInWorkaround) {
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayout(new GridLayout(1, false));
+ Label label = new Label(top, SWT.WRAP);
+ label.setText(
+ "This dialog is shown instead of an inline text editor as a\n" +
+ "workaround for an Eclipse bug specific to OSX Mountain Lion.\n" +
+ "It should be fixed in Eclipse 4.3.");
+ label.setForeground(top.getDisplay().getSystemColor(SWT.COLOR_RED));
+ GridData data = new GridData();
+ data.grabExcessVerticalSpace = false;
+ data.grabExcessHorizontalSpace = false;
+ data.horizontalAlignment = GridData.FILL;
+ data.verticalAlignment = GridData.BEGINNING;
+ label.setLayoutData(data);
+
+ Link link = new Link(top, SWT.NO_FOCUS);
+ link.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
+ link.setText("<a>https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574</a>");
+ link.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ try {
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser();
+ browser.openURL(new URL(event.text));
+ } catch (Exception e) {
+ String message = String.format(
+ "Could not open browser. Vist\n%1$s\ninstead.",
+ event.text);
+ MessageDialog.openError(((Link)event.getSource()).getShell(),
+ "Browser Error", message);
+ }
+ }
+ });
+
+ return top;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java
new file mode 100644
index 000000000..b230aa99d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION;
+import static com.android.SdkConstants.ATTR_HINT;
+import static com.android.SdkConstants.ATTR_TEXT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Extra metadata about properties not available from the descriptors (yet) */
+class PropertyMetadata {
+ static boolean isAdvanced(@NonNull String name) {
+ return sAdvanced.contains(name);
+ }
+
+ static boolean isPreferred(@NonNull String name) {
+ return sPreferred.contains(name);
+ }
+
+ @Nullable
+ static String getCategory(@NonNull String name) {
+ //return sCategories.get(name);
+ assert false : "Disabled to save memory since this method is not currently used.";
+ return null;
+ }
+
+ private static final int ADVANCED_MAP_SIZE = 134;
+ private static final Set<String> sAdvanced = new HashSet<String>(ADVANCED_MAP_SIZE);
+ static {
+ // This metadata about which attributes are "advanced" was generated as follows:
+ // First, I ran the sdk/attribute_stats project with the --list argument to dump out
+ // *all* referenced XML attributes found in layouts, run against a bunch of
+ // sample Android code (development/samples, packages/apps, vendor, etc.
+ //
+ // Then I iterated over the LayoutDescriptors' ViewElementDescriptors'
+ // AttributeDescriptors, and basically diffed the two: any attribute descriptor name
+ // which was *not* found in any of the representative layouts is added here
+ // as an advanced property.
+ //
+ // Then I manually edited in some attributes that were referenced in the sample
+ // layouts but which I still consider to be advanced:
+ // -- nothing right now
+
+ // I also manually *removed* some entries from the below list:
+ // drawableBottom (the others, drawableTop, drawableLeft and drawableRight were all
+ // NOT on the list so keep bottom off for symmetry)
+ // rating (useful when you deal with a RatingsBar component)
+
+
+ // Automatically generated, see above:
+ sAdvanced.add("alwaysDrawnWithCache");
+ sAdvanced.add("animationCache");
+ sAdvanced.add("animationDuration");
+ sAdvanced.add("animationResolution");
+ sAdvanced.add("baseline");
+ sAdvanced.add("bufferType");
+ sAdvanced.add("calendarViewShown");
+ sAdvanced.add("completionHint");
+ sAdvanced.add("completionHintView");
+ sAdvanced.add("completionThreshold");
+ sAdvanced.add("cursorVisible");
+ sAdvanced.add("dateTextAppearance");
+ sAdvanced.add("dial");
+ sAdvanced.add("digits");
+ sAdvanced.add("disableChildrenWhenDisabled");
+ sAdvanced.add("disabledAlpha");
+ sAdvanced.add("drawableAlpha");
+ sAdvanced.add("drawableEnd");
+ sAdvanced.add("drawableStart");
+ sAdvanced.add("drawingCacheQuality");
+ sAdvanced.add("dropDownAnchor");
+ sAdvanced.add("dropDownHeight");
+ sAdvanced.add("dropDownHorizontalOffset");
+ sAdvanced.add("dropDownSelector");
+ sAdvanced.add("dropDownVerticalOffset");
+ sAdvanced.add("dropDownWidth");
+ sAdvanced.add("editorExtras");
+ sAdvanced.add("ems");
+ sAdvanced.add("endYear");
+ sAdvanced.add("eventsInterceptionEnabled");
+ sAdvanced.add("fadeDuration");
+ sAdvanced.add("fadeEnabled");
+ sAdvanced.add("fadeOffset");
+ sAdvanced.add("fadeScrollbars");
+ sAdvanced.add("filterTouchesWhenObscured");
+ sAdvanced.add("firstDayOfWeek");
+ sAdvanced.add("flingable");
+ sAdvanced.add("focusedMonthDateColor");
+ sAdvanced.add("foregroundInsidePadding");
+ sAdvanced.add("format");
+ sAdvanced.add("gestureColor");
+ sAdvanced.add("gestureStrokeAngleThreshold");
+ sAdvanced.add("gestureStrokeLengthThreshold");
+ sAdvanced.add("gestureStrokeSquarenessThreshold");
+ sAdvanced.add("gestureStrokeType");
+ sAdvanced.add("gestureStrokeWidth");
+ sAdvanced.add("hand_hour");
+ sAdvanced.add("hand_minute");
+ sAdvanced.add("hapticFeedbackEnabled");
+ sAdvanced.add("id");
+ sAdvanced.add("imeActionId");
+ sAdvanced.add("imeActionLabel");
+ sAdvanced.add("indeterminateDrawable");
+ sAdvanced.add("indeterminateDuration");
+ sAdvanced.add("inputMethod");
+ sAdvanced.add("interpolator");
+ sAdvanced.add("isScrollContainer");
+ sAdvanced.add("keepScreenOn");
+ sAdvanced.add("layerType");
+ sAdvanced.add("layoutDirection");
+ sAdvanced.add("maxDate");
+ sAdvanced.add("minDate");
+ sAdvanced.add("mode");
+ sAdvanced.add("numeric");
+ sAdvanced.add("paddingEnd");
+ sAdvanced.add("paddingStart");
+ sAdvanced.add("persistentDrawingCache");
+ sAdvanced.add("phoneNumber");
+ sAdvanced.add("popupBackground");
+ sAdvanced.add("popupPromptView");
+ sAdvanced.add("privateImeOptions");
+ sAdvanced.add("quickContactWindowSize");
+ //sAdvanced.add("rating");
+ sAdvanced.add("requiresFadingEdge");
+ sAdvanced.add("rotation");
+ sAdvanced.add("rotationX");
+ sAdvanced.add("rotationY");
+ sAdvanced.add("saveEnabled");
+ sAdvanced.add("scaleX");
+ sAdvanced.add("scaleY");
+ sAdvanced.add("scrollX");
+ sAdvanced.add("scrollY");
+ sAdvanced.add("scrollbarAlwaysDrawHorizontalTrack");
+ sAdvanced.add("scrollbarDefaultDelayBeforeFade");
+ sAdvanced.add("scrollbarFadeDuration");
+ sAdvanced.add("scrollbarSize");
+ sAdvanced.add("scrollbarThumbHorizontal");
+ sAdvanced.add("scrollbarThumbVertical");
+ sAdvanced.add("scrollbarTrackHorizontal");
+ sAdvanced.add("scrollbarTrackVertical");
+ sAdvanced.add("secondaryProgress");
+ sAdvanced.add("selectedDateVerticalBar");
+ sAdvanced.add("selectedWeekBackgroundColor");
+ sAdvanced.add("selectionDivider");
+ sAdvanced.add("selectionDividerHeight");
+ sAdvanced.add("showWeekNumber");
+ sAdvanced.add("shownWeekCount");
+ sAdvanced.add("solidColor");
+ sAdvanced.add("soundEffectsEnabled");
+ sAdvanced.add("spinnerMode");
+ sAdvanced.add("spinnersShown");
+ sAdvanced.add("startYear");
+ sAdvanced.add("switchMinWidth");
+ sAdvanced.add("switchPadding");
+ sAdvanced.add("switchTextAppearance");
+ sAdvanced.add("textColorHighlight");
+ sAdvanced.add("textCursorDrawable");
+ sAdvanced.add("textDirection");
+ sAdvanced.add("textEditNoPasteWindowLayout");
+ sAdvanced.add("textEditPasteWindowLayout");
+ sAdvanced.add("textEditSideNoPasteWindowLayout");
+ sAdvanced.add("textEditSidePasteWindowLayout");
+ sAdvanced.add("textEditSuggestionItemLayout");
+ sAdvanced.add("textIsSelectable");
+ sAdvanced.add("textOff");
+ sAdvanced.add("textOn");
+ sAdvanced.add("textScaleX");
+ sAdvanced.add("textSelectHandle");
+ sAdvanced.add("textSelectHandleLeft");
+ sAdvanced.add("textSelectHandleRight");
+ sAdvanced.add("thumbOffset");
+ sAdvanced.add("thumbTextPadding");
+ sAdvanced.add("tint");
+ sAdvanced.add("track");
+ sAdvanced.add("transformPivotX");
+ sAdvanced.add("transformPivotY");
+ sAdvanced.add("translationX");
+ sAdvanced.add("translationY");
+ sAdvanced.add("uncertainGestureColor");
+ sAdvanced.add("unfocusedMonthDateColor");
+ sAdvanced.add("unselectedAlpha");
+ sAdvanced.add("verticalScrollbarPosition");
+ sAdvanced.add("weekDayTextAppearance");
+ sAdvanced.add("weekNumberColor");
+ sAdvanced.add("weekSeparatorLineColor");
+
+ assert sAdvanced.size() == ADVANCED_MAP_SIZE : sAdvanced.size();
+
+ }
+
+ private static final int PREFERRED_MAP_SIZE = 7;
+ private static final Set<String> sPreferred = new HashSet<String>(PREFERRED_MAP_SIZE);
+ static {
+ // Manual registrations of attributes that should be treated as preferred if
+ // they are available on a widget even if they don't show up in the top 10% of
+ // usages (which the view metadata provides)
+ sPreferred.add(ATTR_TEXT);
+ sPreferred.add(ATTR_CONTENT_DESCRIPTION);
+ sPreferred.add(ATTR_HINT);
+ sPreferred.add("indeterminate");
+ sPreferred.add("progress");
+ sPreferred.add("rating");
+ sPreferred.add("max");
+ assert sPreferred.size() == PREFERRED_MAP_SIZE : sPreferred.size();
+ }
+
+ /*
+ private static final int CATEGORY_MAP_SIZE = 62;
+ private static final Map<String, String> sCategories =
+ new HashMap<String, String>(CATEGORY_MAP_SIZE);
+ static {
+ sCategories.put("requiresFadingEdge", "Scrolling");
+ sCategories.put("fadingEdgeLength", "Scrolling");
+ sCategories.put("scrollbarSize", "Scrolling");
+ sCategories.put("scrollbarThumbVertical", "Scrolling");
+ sCategories.put("scrollbarThumbHorizontal", "Scrolling");
+ sCategories.put("scrollbarTrackHorizontal", "Scrolling");
+ sCategories.put("scrollbarTrackVertical", "Scrolling");
+ sCategories.put("scrollbarAlwaysDrawHorizontalTrack", "Scrolling");
+ sCategories.put("scrollbarAlwaysDrawVerticalTrack", "Scrolling");
+ sCategories.put("scrollViewStyle", "Scrolling");
+ sCategories.put("scrollbars", "Scrolling");
+ sCategories.put("scrollingCache", "Scrolling");
+ sCategories.put("scrollHorizontally", "Scrolling");
+ sCategories.put("scrollbarFadeDuration", "Scrolling");
+ sCategories.put("scrollbarDefaultDelayBeforeFade", "Scrolling");
+ sCategories.put("fastScrollEnabled", "Scrolling");
+ sCategories.put("smoothScrollbar", "Scrolling");
+ sCategories.put("isScrollContainer", "Scrolling");
+ sCategories.put("fadeScrollbars", "Scrolling");
+ sCategories.put("overScrollMode", "Scrolling");
+ sCategories.put("overScrollHeader", "Scrolling");
+ sCategories.put("overScrollFooter", "Scrolling");
+ sCategories.put("verticalScrollbarPosition", "Scrolling");
+ sCategories.put("fastScrollAlwaysVisible", "Scrolling");
+ sCategories.put("fastScrollThumbDrawable", "Scrolling");
+ sCategories.put("fastScrollPreviewBackgroundLeft", "Scrolling");
+ sCategories.put("fastScrollPreviewBackgroundRight", "Scrolling");
+ sCategories.put("fastScrollTrackDrawable", "Scrolling");
+ sCategories.put("fastScrollOverlayPosition", "Scrolling");
+ sCategories.put("horizontalScrollViewStyle", "Scrolling");
+ sCategories.put("fastScrollTextColor", "Scrolling");
+ sCategories.put("scrollbarSize", "Scrolling");
+ sCategories.put("scrollbarSize", "Scrolling");
+ sCategories.put("scrollbarSize", "Scrolling");
+ sCategories.put("scrollbarSize", "Scrolling");
+ sCategories.put("scrollbarSize", "Scrolling");
+
+ // TODO: All the styles: radioButtonStyle, ratingBarStyle, progressBarStyle, ...
+
+ sCategories.put("focusable", "Focus");
+ sCategories.put("focusableInTouchMode", "Focus");
+ sCategories.put("nextFocusLeft", "Focus");
+ sCategories.put("nextFocusRight", "Focus");
+ sCategories.put("nextFocusUp", "Focus");
+ sCategories.put("nextFocusDown", "Focus");
+ sCategories.put("descendantFocusability", "Focus");
+ sCategories.put("selectAllOnFocus", "Focus");
+ sCategories.put("nextFocusForward", "Focus");
+ sCategories.put("colorFocusedHighlight", "Focus");
+
+ sCategories.put("rotation", "Transforms");
+ sCategories.put("scrollX", "Transforms");
+ sCategories.put("scrollY", "Transforms");
+ sCategories.put("rotationX", "Transforms");
+ sCategories.put("rotationY", "Transforms");
+ sCategories.put("transformPivotX", "Transforms");
+ sCategories.put("transformPivotY", "Transforms");
+ sCategories.put("translationX", "Transforms");
+ sCategories.put("translationY", "Transforms");
+ sCategories.put("scaleX", "Transforms");
+ sCategories.put("scaleY", "Transforms");
+
+ sCategories.put("width", "Size");
+ sCategories.put("height", "Size");
+ sCategories.put("minWidth", "Size");
+ sCategories.put("minHeight", "Size");
+
+ sCategories.put("longClickable", "Clicks");
+ sCategories.put("onClick", "Clicks");
+ sCategories.put("clickable", "Clicks");
+ sCategories.put("hapticFeedbackEnabled", "Clicks");
+
+ sCategories.put("duplicateParentState", "State");
+ sCategories.put("addStatesFromChildren", "State");
+
+ assert sCategories.size() == CATEGORY_MAP_SIZE : sCategories.size();
+ }
+ */
+
+// private static final int PRIO_CLZ_LAYOUT = 1000;
+// private static final int PRIO_CLZ_TEXT = 2000;
+// private static final int PRIO_CLZ_DRAWABLE = 3000;
+// private static final int PRIO_CLZ_ANIMATION = 4000;
+// private static final int PRIO_CLZ_FOCUS = 5000;
+//
+// private static final int PRIORITY_MAP_SIZE = 100;
+// private static final Map<String, Integer> sPriorities =
+// new HashMap<String, Integer>(PRIORITY_MAP_SIZE);
+// static {
+// // TODO: I should put all the properties roughly based on their original order: this
+// // will correspond to the rough order they came in with
+// // TODO: How can I make similar complex properties show up adjacent; e.g. min and max
+// sPriorities.put("min", PRIO_CLZ_LAYOUT);
+// sPriorities.put("max", PRIO_CLZ_LAYOUT);
+//
+// assert sPriorities.size() == PRIORITY_MAP_SIZE : sPriorities.size();
+// }
+
+ // TODO: Emit metadata into a file
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java
new file mode 100644
index 000000000..58fddc0ee
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory.SortingMode;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.Page;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.eclipse.wb.internal.core.editor.structure.IPage;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.table.IPropertyExceptionHandler;
+import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Property sheet page used when the graphical layout editor is chosen
+ */
+public class PropertySheetPage extends Page
+ implements IPropertySheetPage, IUiUpdateListener, IPage {
+ private PropertyTable mPropertyTable;
+ private final GraphicalEditorPart mEditor;
+ private Property mActiveProperty;
+ private Action mDefaultValueAction;
+ private Action mShowAdvancedPropertiesAction;
+ private Action mSortAlphaAction;
+ private Action mCollapseAll;
+ private Action mExpandAll;
+ private List<CanvasViewInfo> mSelection;
+
+ private static final String EXPAND_DISABLED_ICON = "expandall-disabled"; //$NON-NLS-1$
+ private static final String EXPAND_ICON = "expandall"; //$NON-NLS-1$
+ private static final String DEFAULT_ICON = "properties_default"; //$NON-NLS-1$
+ private static final String ADVANCED_ICON = "filter_advanced_properties"; //$NON-NLS-1$
+ private static final String ALPHA_ICON = "sort_alpha"; //$NON-NLS-1$
+ // TODO: goto-definition.png
+
+ /**
+ * Constructs a new {@link PropertySheetPage} associated with the given
+ * editor
+ *
+ * @param editor the editor associated with this property sheet page
+ */
+ public PropertySheetPage(GraphicalEditorPart editor) {
+ mEditor = editor;
+ }
+
+ private PropertyFactory getPropertyFactory() {
+ return mEditor.getPropertyFactory();
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ assert parent != null;
+ mPropertyTable = new PropertyTable(parent, SWT.NONE);
+ mPropertyTable.setExceptionHandler(new IPropertyExceptionHandler() {
+ @Override
+ public void handle(Throwable e) {
+ AdtPlugin.log(e, null);
+ }
+ });
+ mPropertyTable.setDefaultCollapsedNames(Arrays.asList(
+ "Deprecated",
+ "Layout Parameters",
+ "Layout Parameters|Margins"));
+
+ createActions();
+ setPropertyTableContextMenu();
+ }
+
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ if (selection instanceof TreeSelection
+ && mPropertyTable != null && !mPropertyTable.isDisposed()) {
+ TreeSelection treeSelection = (TreeSelection) selection;
+
+ // We get a lot of repeated selection requests for the same selection
+ // as before, so try to eliminate these
+ if (mSelection != null) {
+ if (mSelection.isEmpty()) {
+ if (treeSelection.isEmpty()) {
+ return;
+ }
+ } else {
+ int selectionCount = treeSelection.size();
+ if (selectionCount == mSelection.size()) {
+ boolean same = true;
+ Iterator<?> iterator = treeSelection.iterator();
+ for (int i = 0, n = selectionCount; i < n && iterator.hasNext(); i++) {
+ Object next = iterator.next();
+ if (next instanceof CanvasViewInfo) {
+ CanvasViewInfo info = (CanvasViewInfo) next;
+ if (info != mSelection.get(i)) {
+ same = false;
+ break;
+ }
+ } else {
+ same = false;
+ break;
+ }
+ }
+ if (same) {
+ return;
+ }
+ }
+ }
+ }
+
+ stopTrackingSelection();
+
+ if (treeSelection.isEmpty()) {
+ mSelection = Collections.emptyList();
+ } else {
+ int selectionCount = treeSelection.size();
+ List<CanvasViewInfo> newSelection = new ArrayList<CanvasViewInfo>(selectionCount);
+ Iterator<?> iterator = treeSelection.iterator();
+ while (iterator.hasNext()) {
+ Object next = iterator.next();
+ if (next instanceof CanvasViewInfo) {
+ CanvasViewInfo info = (CanvasViewInfo) next;
+ newSelection.add(info);
+ }
+ }
+ mSelection = newSelection;
+ }
+
+ startTrackingSelection();
+
+ refreshProperties();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ stopTrackingSelection();
+ super.dispose();
+ }
+
+ private void startTrackingSelection() {
+ if (mSelection != null && !mSelection.isEmpty()) {
+ for (CanvasViewInfo item : mSelection) {
+ UiViewElementNode node = item.getUiViewNode();
+ if (node != null) {
+ node.addUpdateListener(this);
+ }
+ }
+ }
+ }
+
+ private void stopTrackingSelection() {
+ if (mSelection != null && !mSelection.isEmpty()) {
+ for (CanvasViewInfo item : mSelection) {
+ UiViewElementNode node = item.getUiViewNode();
+ if (node != null) {
+ node.removeUpdateListener(this);
+ }
+ }
+ }
+ mSelection = null;
+ }
+
+ // Implements IUiUpdateListener
+ @Override
+ public void uiElementNodeUpdated(UiElementNode node, UiUpdateState state) {
+ refreshProperties();
+ }
+
+ @Override
+ public Control getControl() {
+ return mPropertyTable;
+ }
+
+ @Override
+ public void setFocus() {
+ mPropertyTable.setFocus();
+ }
+
+ @Override
+ public void makeContributions(IMenuManager menuManager,
+ IToolBarManager toolBarManager, IStatusLineManager statusLineManager) {
+ toolBarManager.add(mShowAdvancedPropertiesAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mSortAlphaAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mDefaultValueAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mExpandAll);
+ toolBarManager.add(mCollapseAll);
+ toolBarManager.add(new Separator());
+ }
+
+ private void createActions() {
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ IconFactory iconFactory = IconFactory.getInstance();
+
+ mExpandAll = new PropertySheetAction(
+ IAction.AS_PUSH_BUTTON,
+ "Expand All",
+ ACTION_EXPAND,
+ iconFactory.getImageDescriptor(EXPAND_ICON),
+ iconFactory.getImageDescriptor(EXPAND_DISABLED_ICON));
+
+ mCollapseAll = new PropertySheetAction(
+ IAction.AS_PUSH_BUTTON,
+ "Collapse All",
+ ACTION_COLLAPSE,
+ sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL),
+ sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL_DISABLED));
+
+ mShowAdvancedPropertiesAction = new PropertySheetAction(
+ IAction.AS_CHECK_BOX,
+ "Show Advanced Properties",
+ ACTION_SHOW_ADVANCED,
+ iconFactory.getImageDescriptor(ADVANCED_ICON),
+ null);
+
+ mSortAlphaAction = new PropertySheetAction(
+ IAction.AS_CHECK_BOX,
+ "Sort Alphabetically",
+ ACTION_SORT_ALPHA,
+ iconFactory.getImageDescriptor(ALPHA_ICON),
+ null);
+
+ mDefaultValueAction = new PropertySheetAction(
+ IAction.AS_PUSH_BUTTON,
+ "Restore Default Value",
+ ACTION_DEFAULT_VALUE,
+ iconFactory.getImageDescriptor(DEFAULT_ICON),
+ null);
+
+ // Listen on the selection in the property sheet so we can update the
+ // Restore Default Value action
+ ISelectionChangedListener listener = new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ StructuredSelection selection = (StructuredSelection) event.getSelection();
+ mActiveProperty = (Property) selection.getFirstElement();
+ updateDefaultValueAction();
+ }
+ };
+ mPropertyTable.addSelectionChangedListener(listener);
+ }
+
+ /**
+ * Updates the state of {@link #mDefaultValueAction}.
+ */
+ private void updateDefaultValueAction() {
+ if (mActiveProperty != null) {
+ try {
+ mDefaultValueAction.setEnabled(mActiveProperty.isModified());
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+ } else {
+ mDefaultValueAction.setEnabled(false);
+ }
+ }
+
+ /**
+ * Sets the context menu for {@link #mPropertyTable}.
+ */
+ private void setPropertyTableContextMenu() {
+ final MenuManager manager = new MenuManager();
+ manager.setRemoveAllWhenShown(true);
+ manager.addMenuListener(new IMenuListener() {
+ @Override
+ public void menuAboutToShow(IMenuManager m) {
+ // dispose items to avoid caching
+ for (MenuItem item : manager.getMenu().getItems()) {
+ item.dispose();
+ }
+ // apply new items
+ fillContextMenu();
+ }
+
+ private void fillContextMenu() {
+ manager.add(mDefaultValueAction);
+ manager.add(mSortAlphaAction);
+ manager.add(mShowAdvancedPropertiesAction);
+ }
+ });
+
+ mPropertyTable.setMenu(manager.createContextMenu(mPropertyTable));
+ }
+
+ /**
+ * Shows {@link Property}'s of current objects.
+ */
+ private void refreshProperties() {
+ PropertyFactory factory = getPropertyFactory();
+ mPropertyTable.setInput(factory.getProperties(mSelection));
+ updateDefaultValueAction();
+ }
+
+ // ---- Actions ----
+
+ private static final int ACTION_DEFAULT_VALUE = 1;
+ private static final int ACTION_SHOW_ADVANCED = 2;
+ private static final int ACTION_COLLAPSE = 3;
+ private static final int ACTION_EXPAND = 4;
+ private static final int ACTION_SORT_ALPHA = 5;
+
+ private class PropertySheetAction extends Action {
+ private final int mAction;
+
+ private PropertySheetAction(int style, String label, int action,
+ ImageDescriptor imageDesc, ImageDescriptor disabledImageDesc) {
+ super(label, style);
+ mAction = action;
+ setImageDescriptor(imageDesc);
+ if (disabledImageDesc != null) {
+ setDisabledImageDescriptor(disabledImageDesc);
+ }
+ setToolTipText(label);
+ }
+
+ @Override
+ public void run() {
+ switch (mAction) {
+ case ACTION_COLLAPSE: {
+ mPropertyTable.collapseAll();
+ break;
+ }
+ case ACTION_EXPAND: {
+ mPropertyTable.expandAll();
+ break;
+ }
+ case ACTION_SHOW_ADVANCED: {
+ boolean show = mShowAdvancedPropertiesAction.isChecked();
+ mPropertyTable.setShowAdvancedProperties(show);
+ break;
+ }
+ case ACTION_SORT_ALPHA: {
+ boolean isAlphabetical = mSortAlphaAction.isChecked();
+ getPropertyFactory().setSortingMode(
+ isAlphabetical ? SortingMode.ALPHABETICAL : PropertyFactory.DEFAULT_MODE);
+ refreshProperties();
+ break;
+ }
+ case ACTION_DEFAULT_VALUE:
+ try {
+ mActiveProperty.setValue(Property.UNKNOWN_VALUE);
+ } catch (Exception e) {
+ // Ignore warnings from setters
+ }
+ break;
+ default:
+ assert false : mAction;
+ }
+ }
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ makeContributions(null, toolBarManager, null);
+ toolBarManager.update(false);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java
new file mode 100644
index 000000000..f2bf07312
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+
+class PropertyValueCompleter extends ValueCompleter {
+ private final XmlProperty mProperty;
+
+ PropertyValueCompleter(XmlProperty property) {
+ mProperty = property;
+ }
+
+ @Override
+ @Nullable
+ protected CommonXmlEditor getEditor() {
+ return mProperty.getXmlEditor();
+ }
+
+ @Override
+ @NonNull
+ protected AttributeDescriptor getDescriptor() {
+ return mProperty.getDescriptor();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java
new file mode 100644
index 000000000..081ec8069
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.ANDROID_PKG;
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceType;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.fieldassist.ContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Resource value completion for the given property
+ * <p>
+ * TODO:
+ * <ul>
+ * <li>also offer other values seen in the app
+ * <li>also offer previously set values for this property
+ * <li>also complete on properties
+ * </ul>
+ */
+class ResourceValueCompleter implements IContentProposalProvider {
+ protected final XmlProperty xmlProperty;
+
+ ResourceValueCompleter(XmlProperty xmlProperty) {
+ this.xmlProperty = xmlProperty;
+ }
+
+ @Override
+ public IContentProposal[] getProposals(String contents, int position) {
+ if (contents.startsWith(PREFIX_RESOURCE_REF)) {
+ CommonXmlEditor editor = this.xmlProperty.getXmlEditor();
+ if (editor != null) {
+ String[] matches = computeResourceStringMatches(
+ editor,
+ this.xmlProperty.mDescriptor, contents.substring(0, position));
+ List<IContentProposal> proposals = null;
+ if (matches != null && matches.length > 0) {
+ proposals = new ArrayList<IContentProposal>(matches.length);
+ for (String match : matches) {
+ proposals.add(new ContentProposal(match));
+ }
+ return proposals.toArray(new IContentProposal[proposals.size()]);
+ }
+ }
+ }
+
+ return new IContentProposal[0];
+ }
+
+ /**
+ * Similar to {@link UiResourceAttributeNode#computeResourceStringMatches}
+ * but computes complete results up front rather than dividing it up into
+ * smaller chunks like @{code @android:}, {@code string/}, and {@code ok}.
+ */
+ static String[] computeResourceStringMatches(AndroidXmlEditor editor,
+ AttributeDescriptor attributeDescriptor, String prefix) {
+ List<String> results = new ArrayList<String>(200);
+
+ // System matches: only do this if the value already matches at least @a,
+ // and doesn't start with something that can't possibly be @android
+ if (prefix.startsWith("@a") && //$NON-NLS-1$
+ prefix.regionMatches(true /* ignoreCase */, 0, ANDROID_PREFIX, 0,
+ Math.min(prefix.length() - 1, ANDROID_PREFIX.length()))) {
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ ResourceRepository repository = data.getFrameworkResources();
+ addMatches(repository, prefix, true /* isSystem */, results);
+ }
+ } else if (prefix.startsWith("?") && //$NON-NLS-1$
+ prefix.regionMatches(true /* ignoreCase */, 0, ANDROID_THEME_PREFIX, 0,
+ Math.min(prefix.length() - 1, ANDROID_THEME_PREFIX.length()))) {
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ ResourceRepository repository = data.getFrameworkResources();
+ addMatches(repository, prefix, true /* isSystem */, results);
+ }
+ }
+
+
+ // When completing project resources skip framework resources unless
+ // the prefix possibly completes both, such as "@an" which can match
+ // both the project resource @animator as well as @android:string
+ if (!prefix.startsWith("@and") && !prefix.startsWith("?and")) { //$NON-NLS-1$ //$NON-NLS-2$
+ IProject project = editor.getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceManager manager = ResourceManager.getInstance();
+ ResourceRepository repository = manager.getProjectResources(project);
+ if (repository != null) {
+ // We have a style name and a repository. Find all resources that match this
+ // type and recreate suggestions out of them.
+ addMatches(repository, prefix, false /* isSystem */, results);
+ }
+
+ }
+ }
+
+ if (attributeDescriptor != null) {
+ UiResourceAttributeNode.sortAttributeChoices(attributeDescriptor, results);
+ } else {
+ Collections.sort(results);
+ }
+
+ return results.toArray(new String[results.size()]);
+ }
+
+ private static void addMatches(ResourceRepository repository, String prefix, boolean isSystem,
+ List<String> results) {
+ int typeStart = isSystem
+ ? ANDROID_PREFIX.length() : PREFIX_RESOURCE_REF.length();
+
+ for (ResourceType type : repository.getAvailableResourceTypes()) {
+ if (prefix.regionMatches(typeStart, type.getName(), 0,
+ Math.min(type.getName().length(), prefix.length() - typeStart))) {
+ StringBuilder sb = new StringBuilder();
+ if (prefix.length() == 0 || prefix.startsWith(PREFIX_RESOURCE_REF)) {
+ sb.append(PREFIX_RESOURCE_REF);
+ } else {
+ if (type != ResourceType.ATTR) {
+ continue;
+ }
+ sb.append(PREFIX_THEME_REF);
+ }
+
+ if (type == ResourceType.ID && prefix.startsWith(NEW_ID_PREFIX)) {
+ sb.append('+');
+ }
+
+ if (isSystem) {
+ sb.append(ANDROID_PKG).append(':');
+ }
+
+ sb.append(type.getName()).append('/');
+ String base = sb.toString();
+
+ int nameStart = typeStart + type.getName().length() + 1; // +1: add "/" divider
+ String namePrefix =
+ prefix.length() <= nameStart ? "" : prefix.substring(nameStart);
+ for (ResourceItem item : repository.getResourceItemsOfType(type)) {
+ String name = item.getName();
+ if (SdkUtils.startsWithIgnoreCase(name, namePrefix)) {
+ results.add(base + name);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java
new file mode 100644
index 000000000..fb7e45902
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.editor.string.StringPropertyDialog;
+
+class StringXmlPropertyDialog extends StringPropertyDialog {
+ StringXmlPropertyDialog(Shell parentShell, Property property) throws Exception {
+ super(parentShell, property);
+ }
+
+ @Override
+ protected boolean isMultiLine() {
+ return false;
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite area = (Composite) super.createDialogArea(parent);
+
+ Composite workaround = PropertyFactory.addWorkaround(area);
+ if (workaround != null) {
+ workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
+ }
+
+ return area;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java
new file mode 100644
index 000000000..5559349fc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.ATTR_TEXT_SIZE;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+import static com.android.SdkConstants.UNIT_DP;
+import static com.android.SdkConstants.UNIT_SP;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.ide.common.api.IAttributeInfo.Format.BOOLEAN;
+import static com.android.ide.common.api.IAttributeInfo.Format.DIMENSION;
+import static com.android.ide.common.api.IAttributeInfo.Format.ENUM;
+import static com.android.ide.common.api.IAttributeInfo.Format.FLAG;
+import static com.android.ide.common.api.IAttributeInfo.Format.FLOAT;
+import static com.android.ide.common.api.IAttributeInfo.Format.INTEGER;
+import static com.android.ide.common.api.IAttributeInfo.Format.REFERENCE;
+import static com.android.ide.common.api.IAttributeInfo.Format.STRING;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.jface.fieldassist.ContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * An {@link IContentProposalProvider} which completes possible property values
+ * for Android properties, completing resource strings, flag values, enum
+ * values, as well as dimension units.
+ */
+abstract class ValueCompleter implements IContentProposalProvider {
+ @Nullable
+ protected abstract CommonXmlEditor getEditor();
+
+ @NonNull
+ protected abstract AttributeDescriptor getDescriptor();
+
+ @Override
+ public IContentProposal[] getProposals(String contents, int position) {
+ AttributeDescriptor descriptor = getDescriptor();
+ IAttributeInfo info = descriptor.getAttributeInfo();
+ EnumSet<Format> formats = info.getFormats();
+
+ List<IContentProposal> proposals = new ArrayList<IContentProposal>();
+
+ String prefix = contents; // TODO: Go back to position inside the array?
+
+ // TODO: If the user is typing in a number, or a number plus a prefix of a dimension unit,
+ // then propose that number plus the completed dimension unit (using sp for text, dp
+ // for other properties and maybe both if I'm not sure)
+ if (formats.contains(STRING)
+ && !contents.isEmpty()
+ && (formats.size() > 1 && formats.contains(REFERENCE) ||
+ formats.size() > 2)
+ && !contents.startsWith(PREFIX_RESOURCE_REF)
+ && !contents.startsWith(PREFIX_THEME_REF)) {
+ proposals.add(new ContentProposal(contents));
+ }
+
+ if (!contents.isEmpty() && Character.isDigit(contents.charAt(0))
+ && (formats.contains(DIMENSION)
+ || formats.contains(INTEGER)
+ || formats.contains(FLOAT))) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = contents.length(); i < n; i++) {
+ char c = contents.charAt(i);
+ if (Character.isDigit(c)) {
+ sb.append(c);
+ } else {
+ break;
+ }
+ }
+
+ String number = sb.toString();
+ if (formats.contains(Format.DIMENSION)) {
+ if (descriptor.getXmlLocalName().equals(ATTR_TEXT_SIZE)) {
+ proposals.add(new ContentProposal(number + UNIT_SP));
+ }
+ proposals.add(new ContentProposal(number + UNIT_DP));
+ } else if (formats.contains(Format.INTEGER)) {
+ proposals.add(new ContentProposal(number));
+ }
+ // Perhaps offer other units too -- see AndroidContentAssist.sDimensionUnits
+ }
+
+ if (formats.contains(REFERENCE) || contents.startsWith(PREFIX_RESOURCE_REF)
+ || contents.startsWith(PREFIX_THEME_REF)) {
+ CommonXmlEditor editor = getEditor();
+ if (editor != null) {
+ String[] matches = ResourceValueCompleter.computeResourceStringMatches(
+ editor,
+ descriptor, contents.substring(0, position));
+ for (String match : matches) {
+ proposals.add(new ContentProposal(match));
+ }
+ }
+ }
+
+ if (formats.contains(FLAG)) {
+ String[] values = info.getFlagValues();
+ if (values != null) {
+ // Flag completion
+ int flagStart = prefix.lastIndexOf('|');
+ String prepend = null;
+ if (flagStart != -1) {
+ prepend = prefix.substring(0, flagStart + 1);
+ prefix = prefix.substring(flagStart + 1).trim();
+ }
+
+ boolean exactMatch = false;
+ for (String value : values) {
+ if (prefix.equals(value)) {
+ exactMatch = true;
+ proposals.add(new ContentProposal(contents));
+
+ break;
+ }
+ }
+
+ if (exactMatch) {
+ prepend = contents + '|';
+ prefix = "";
+ }
+
+ for (String value : values) {
+ if (SdkUtils.startsWithIgnoreCase(value, prefix)) {
+ if (prepend != null && prepend.contains(value)) {
+ continue;
+ }
+ String match;
+ if (prepend != null) {
+ match = prepend + value;
+ } else {
+ match = value;
+ }
+ proposals.add(new ContentProposal(match));
+ }
+ }
+ }
+ } else if (formats.contains(ENUM)) {
+ String[] values = info.getEnumValues();
+ if (values != null) {
+ for (String value : values) {
+ if (SdkUtils.startsWithIgnoreCase(value, prefix)) {
+ proposals.add(new ContentProposal(value));
+ }
+ }
+
+ for (String value : values) {
+ if (!SdkUtils.startsWithIgnoreCase(value, prefix)) {
+ proposals.add(new ContentProposal(value));
+ }
+ }
+ }
+ } else if (formats.contains(BOOLEAN)) {
+ proposals.add(new ContentProposal(VALUE_TRUE));
+ proposals.add(new ContentProposal(VALUE_FALSE));
+ }
+
+ return proposals.toArray(new IContentProposal[proposals.size()]);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java
new file mode 100644
index 000000000..a320b682d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor;
+import org.eclipse.wb.internal.core.model.property.table.PropertyTooltipProvider;
+import org.eclipse.wb.internal.core.model.property.table.PropertyTooltipTextProvider;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+
+import java.util.Map;
+
+/**
+ * An Android XML property
+ */
+class XmlProperty extends Property {
+ private PropertyFactory mFactory;
+ final AttributeDescriptor mDescriptor;
+ private UiViewElementNode mNode;
+ private Property mParent;
+
+ XmlProperty(
+ @NonNull PropertyEditor editor,
+ @NonNull PropertyFactory factory,
+ @NonNull UiViewElementNode node,
+ @NonNull AttributeDescriptor descriptor) {
+ super(editor);
+ mFactory = factory;
+ mNode = node;
+ mDescriptor = descriptor;
+ }
+
+ @NonNull
+ public PropertyFactory getFactory() {
+ return mFactory;
+ }
+
+ @NonNull
+ public UiViewElementNode getNode() {
+ return mNode;
+ }
+
+ @NonNull
+ public AttributeDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ @Override
+ @NonNull
+ public String getName() {
+ return mDescriptor.getXmlLocalName();
+ }
+
+ @Override
+ @NonNull
+ public String getTitle() {
+ String name = mDescriptor.getXmlLocalName();
+ int nameLength = name.length();
+
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ if (name.startsWith(ATTR_LAYOUT_MARGIN)
+ && nameLength > ATTR_LAYOUT_MARGIN.length()) {
+ name = name.substring(ATTR_LAYOUT_MARGIN.length());
+ } else {
+ name = name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ }
+
+ // Capitalize
+ name = DescriptorsUtils.capitalize(name);
+
+ // If we're nested within a complex property, say "Line Spacing", don't
+ // include "Line Spacing " as a prefix for each property here
+ if (mParent != null) {
+ String parentTitle = mParent.getTitle();
+ if (name.startsWith(parentTitle)) {
+ int parentTitleLength = parentTitle.length();
+ if (parentTitleLength < nameLength) {
+ if (nameLength > parentTitleLength &&
+ Character.isWhitespace(name.charAt(parentTitleLength))) {
+ parentTitleLength++;
+ }
+ name = name.substring(parentTitleLength);
+ }
+ }
+ }
+
+ return name;
+ }
+
+ @Override
+ public <T> T getAdapter(Class<T> adapter) {
+ // tooltip
+ if (adapter == PropertyTooltipProvider.class) {
+ return adapter.cast(new PropertyTooltipTextProvider() {
+ @Override
+ protected String getText(Property p) throws Exception {
+ if (mDescriptor instanceof IPropertyDescriptor) {
+ IPropertyDescriptor d = (IPropertyDescriptor) mDescriptor;
+ return d.getDescription();
+ }
+
+ return null;
+ }
+ });
+ } else if (adapter == IContentProposalProvider.class) {
+ IAttributeInfo info = mDescriptor.getAttributeInfo();
+ if (info != null) {
+ return adapter.cast(new PropertyValueCompleter(this));
+ }
+ // Fallback: complete values on resource values
+ return adapter.cast(new ResourceValueCompleter(this));
+ } else if (adapter == ILabelProvider.class) {
+ return adapter.cast(new LabelProvider() {
+ @Override
+ public Image getImage(Object element) {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ @Override
+ public String getText(Object element) {
+ return ((IContentProposal) element).getLabel();
+ }
+ });
+ }
+ return super.getAdapter(adapter);
+ }
+
+ @Override
+ public boolean isModified() throws Exception {
+ Object s = null;
+ try {
+ Element element = (Element) mNode.getXmlNode();
+ if (element == null) {
+ return false;
+ }
+ String name = mDescriptor.getXmlLocalName();
+ String uri = mDescriptor.getNamespaceUri();
+ if (uri != null) {
+ return element.hasAttributeNS(uri, name);
+ } else {
+ return element.hasAttribute(name);
+ }
+ } catch (Exception e) {
+ // pass
+ }
+ return s != null && s.toString().length() > 0;
+ }
+
+ @Nullable
+ public String getStringValue() {
+ Element element = (Element) mNode.getXmlNode();
+ if (element == null) {
+ return null;
+ }
+ String name = mDescriptor.getXmlLocalName();
+ String uri = mDescriptor.getNamespaceUri();
+ Attr attr;
+ if (uri != null) {
+ attr = element.getAttributeNodeNS(uri, name);
+ } else {
+ attr = element.getAttributeNode(name);
+ }
+ if (attr != null) {
+ return attr.getValue();
+ }
+
+ Object viewObject = getFactory().getCurrentViewObject();
+ if (viewObject != null) {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ if (graphicalEditor == null) {
+ return null;
+ }
+ ViewHierarchy views = graphicalEditor.getCanvasControl().getViewHierarchy();
+ Map<String, String> defaultProperties = views.getDefaultProperties(viewObject);
+ if (defaultProperties != null) {
+ return defaultProperties.get(name);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Object getValue() throws Exception {
+ return getStringValue();
+ }
+
+ @Override
+ public void setValue(Object value) throws Exception {
+ CommonXmlEditor editor = getXmlEditor();
+ if (editor == null) {
+ return;
+ }
+ final String attribute = mDescriptor.getXmlLocalName();
+ final String xmlValue = value != null && value != UNKNOWN_VALUE ? value.toString() : null;
+ editor.wrapUndoEditXmlModel(
+ String.format("Set \"%1$s\" to \"%2$s\"", attribute, xmlValue),
+ new Runnable() {
+ @Override
+ public void run() {
+ mNode.setAttributeValue(attribute,
+ mDescriptor.getNamespaceUri(), xmlValue, true /*override*/);
+ mNode.commitDirtyAttributesToXml();
+ }
+ });
+ }
+
+ @Override
+ @NonNull
+ public Property getComposite(Property[] properties) {
+ return XmlPropertyComposite.create(properties);
+ }
+
+ @Nullable
+ GraphicalEditorPart getGraphicalEditor() {
+ return mFactory.getGraphicalEditor();
+ }
+
+ @Nullable
+ CommonXmlEditor getXmlEditor() {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ if (graphicalEditor != null) {
+ return graphicalEditor.getEditorDelegate().getEditor();
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public Property getParent() {
+ return mParent;
+ }
+
+ public void setParent(@Nullable Property parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public String toString() {
+ return getName() + ":" + getPriority();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java
new file mode 100644
index 000000000..af9e13b3e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import com.android.annotations.NonNull;
+import com.google.common.base.Objects;
+
+import org.eclipse.wb.internal.core.model.property.Property;
+
+import java.util.Arrays;
+
+/**
+ * Property holding multiple instances of the same {@link XmlProperty} (but
+ * bound to difference objects. This is used when multiple objects are selected
+ * in the layout editor and the common properties are shown; editing a value
+ * will (via {@link #setValue(Object)}) set it on all selected objects.
+ * <p>
+ * Similar to
+ * org.eclipse.wb.internal.core.model.property.GenericPropertyComposite
+ */
+class XmlPropertyComposite extends XmlProperty {
+ private static final Object NO_VALUE = new Object();
+
+ private final XmlProperty[] mProperties;
+
+ public XmlPropertyComposite(XmlProperty primary, XmlProperty[] properties) {
+ super(
+ primary.getEditor(),
+ primary.getFactory(),
+ primary.getNode(),
+ primary.getDescriptor());
+ mProperties = properties;
+ }
+
+ @Override
+ @NonNull
+ public String getTitle() {
+ return mProperties[0].getTitle();
+ }
+
+ @Override
+ public int hashCode() {
+ return mProperties.length;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (obj instanceof XmlPropertyComposite) {
+ XmlPropertyComposite property = (XmlPropertyComposite) obj;
+ return Arrays.equals(mProperties, property.mProperties);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isModified() throws Exception {
+ for (Property property : mProperties) {
+ if (property.isModified()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public Object getValue() throws Exception {
+ Object value = NO_VALUE;
+ for (Property property : mProperties) {
+ Object propertyValue = property.getValue();
+ if (value == NO_VALUE) {
+ value = propertyValue;
+ } else if (!Objects.equal(value, propertyValue)) {
+ return UNKNOWN_VALUE;
+ }
+ }
+
+ return value;
+ }
+
+ @Override
+ public void setValue(final Object value) throws Exception {
+ // TBD: Wrap in ExecutionUtils.run?
+ for (Property property : mProperties) {
+ property.setValue(value);
+ }
+ }
+
+ @NonNull
+ public static XmlPropertyComposite create(Property... properties) {
+ // Cast from Property into XmlProperty
+ XmlProperty[] xmlProperties = new XmlProperty[properties.length];
+ for (int i = 0; i < properties.length; i++) {
+ Property property = properties[i];
+ xmlProperties[i] = (XmlProperty) property;
+ }
+
+ XmlPropertyComposite composite = new XmlPropertyComposite(xmlProperties[0], xmlProperties);
+ composite.setCategory(xmlProperties[0].getCategory());
+ return composite;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java
new file mode 100644
index 000000000..87fb0e6ed
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.properties;
+
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.DOT_PNG;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper;
+import com.android.resources.ResourceType;
+import com.google.common.collect.Maps;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.MessageDialogWithToggle;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.wb.draw2d.IColorConstants;
+import org.eclipse.wb.internal.core.model.property.Property;
+import org.eclipse.wb.internal.core.model.property.editor.AbstractTextPropertyEditor;
+import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation;
+import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation;
+import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
+import org.eclipse.wb.internal.core.utils.ui.DrawUtils;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Special property editor used for the {@link XmlProperty} instances which handles
+ * editing the XML properties, rendering defaults by looking up the actual colors and images,
+ */
+class XmlPropertyEditor extends AbstractTextPropertyEditor {
+ public static final XmlPropertyEditor INSTANCE = new XmlPropertyEditor();
+ private static final int SAMPLE_SIZE = 10;
+ private static final int SAMPLE_MARGIN = 3;
+
+ protected XmlPropertyEditor() {
+ }
+
+ private final PropertyEditorPresentation mPresentation =
+ new ButtonPropertyEditorPresentation() {
+ @Override
+ protected void onClick(PropertyTable propertyTable, Property property) throws Exception {
+ openDialog(propertyTable, property);
+ }
+ };
+
+ @Override
+ public PropertyEditorPresentation getPresentation() {
+ return mPresentation;
+ }
+
+ @Override
+ public String getText(Property property) throws Exception {
+ Object value = property.getValue();
+ if (value instanceof String) {
+ return (String) value;
+ }
+ return null;
+ }
+
+ @Override
+ protected String getEditorText(Property property) throws Exception {
+ return getText(property);
+ }
+
+ @Override
+ public void paint(Property property, GC gc, int x, int y, int width, int height)
+ throws Exception {
+ String text = getText(property);
+ if (text != null) {
+ ResourceValue resValue = null;
+ String resolvedText = null;
+
+ // TODO: Use the constants for @, ?, @android: etc
+ if (text.startsWith("@") || text.startsWith("?")) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Yes, try to resolve it in order to show better info
+ XmlProperty xmlProperty = (XmlProperty) property;
+ GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
+ if (graphicalEditor != null) {
+ ResourceResolver resolver = graphicalEditor.getResourceResolver();
+ boolean isFramework = text.startsWith(ANDROID_PREFIX)
+ || text.startsWith(ANDROID_THEME_PREFIX);
+ resValue = resolver.findResValue(text, isFramework);
+ while (resValue != null && resValue.getValue() != null) {
+ String value = resValue.getValue();
+ if (value.startsWith(PREFIX_RESOURCE_REF)
+ || value.startsWith(PREFIX_THEME_REF)) {
+ // TODO: do I have to strip off the @ too?
+ isFramework = isFramework
+ || value.startsWith(ANDROID_PREFIX)
+ || value.startsWith(ANDROID_THEME_PREFIX);
+ ResourceValue v = resolver.findResValue(text, isFramework);
+ if (v != null && !value.equals(v.getValue())) {
+ resValue = v;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ } else if (text.startsWith("#") && text.matches("#\\p{XDigit}+")) { //$NON-NLS-1$
+ resValue = new ResourceValue(ResourceType.COLOR, property.getName(), text, false);
+ }
+
+ if (resValue != null && resValue.getValue() != null) {
+ String value = resValue.getValue();
+ // Decide whether it's a color, an image, a nine patch etc
+ // and decide how to render it
+ if (value.startsWith("#") || value.endsWith(DOT_XML) //$NON-NLS-1$
+ && value.contains("res/color")) { //$NON-NLS-1$ // TBD: File.separator?
+ XmlProperty xmlProperty = (XmlProperty) property;
+ GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
+ if (graphicalEditor != null) {
+ ResourceResolver resolver = graphicalEditor.getResourceResolver();
+ RGB rgb = ResourceHelper.resolveColor(resolver, resValue);
+ if (rgb != null) {
+ Color color = new Color(gc.getDevice(), rgb);
+ // draw color sample
+ Color oldBackground = gc.getBackground();
+ Color oldForeground = gc.getForeground();
+ try {
+ int width_c = SAMPLE_SIZE;
+ int height_c = SAMPLE_SIZE;
+ int x_c = x;
+ int y_c = y + (height - height_c) / 2;
+ // update rest bounds
+ int delta = SAMPLE_SIZE + SAMPLE_MARGIN;
+ x += delta;
+ width -= delta;
+ // fill
+ gc.setBackground(color);
+ gc.fillRectangle(x_c, y_c, width_c, height_c);
+ // draw line
+ gc.setForeground(IColorConstants.gray);
+ gc.drawRectangle(x_c, y_c, width_c, height_c);
+ } finally {
+ gc.setBackground(oldBackground);
+ gc.setForeground(oldForeground);
+ }
+ color.dispose();
+ }
+ }
+ } else {
+ Image swtImage = null;
+ if (value.endsWith(DOT_XML) && value.contains("res/drawable")) { // TBD: Filesep?
+ Map<String, Image> cache = getImageCache(property);
+ swtImage = cache.get(value);
+ if (swtImage == null) {
+ XmlProperty xmlProperty = (XmlProperty) property;
+ GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
+ RenderService service = RenderService.create(graphicalEditor);
+ service.setOverrideRenderSize(SAMPLE_SIZE, SAMPLE_SIZE);
+ BufferedImage drawable = service.renderDrawable(resValue);
+ if (drawable != null) {
+ swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable,
+ true /*transferAlpha*/, -1);
+ cache.put(value, swtImage);
+ }
+ }
+ } else if (value.endsWith(DOT_PNG)) {
+ // TODO: 9-patch handling?
+ //if (text.endsWith(DOT_9PNG)) {
+ // // 9-patch image: How do we paint this?
+ // URL url = new File(text).toURI().toURL();
+ // NinePatch ninepatch = NinePatch.load(url, false /* ?? */);
+ // BufferedImage image = ninepatch.getImage();
+ //}
+ Map<String, Image> cache = getImageCache(property);
+ swtImage = cache.get(value);
+ if (swtImage == null) {
+ File file = new File(value);
+ if (file.exists()) {
+ try {
+ BufferedImage awtImage = ImageIO.read(file);
+ if (awtImage != null && awtImage.getWidth() > 0
+ && awtImage.getHeight() > 0) {
+ awtImage = ImageUtils.cropBlank(awtImage, null);
+ if (awtImage != null) {
+ // Scale image
+ int imageWidth = awtImage.getWidth();
+ int imageHeight = awtImage.getHeight();
+ int maxWidth = 3 * height;
+
+ if (imageWidth > maxWidth || imageHeight > height) {
+ double scale = height / (double) imageHeight;
+ int scaledWidth = (int) (imageWidth * scale);
+ if (scaledWidth > maxWidth) {
+ scale = maxWidth / (double) imageWidth;
+ }
+ awtImage = ImageUtils.scale(awtImage, scale,
+ scale);
+ }
+ swtImage = SwtUtils.convertToSwt(gc.getDevice(),
+ awtImage, true /*transferAlpha*/, -1);
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, value);
+ }
+ }
+ cache.put(value, swtImage);
+ }
+
+ } else if (value != null) {
+ // It's a normal string: if different from the text, paint
+ // it in parentheses, e.g.
+ // @string/foo: Foo Bar (probably cropped)
+ if (!value.equals(text) && !value.equals("@null")) { //$NON-NLS-1$
+ resolvedText = value;
+ }
+ }
+
+ if (swtImage != null) {
+ // Make a square the size of the height
+ ImageData imageData = swtImage.getImageData();
+ int imageWidth = imageData.width;
+ int imageHeight = imageData.height;
+ if (imageWidth > 0 && imageHeight > 0) {
+ gc.drawImage(swtImage, x, y + (height - imageHeight) / 2);
+ int delta = imageWidth + SAMPLE_MARGIN;
+ x += delta;
+ width -= delta;
+ }
+ }
+ }
+ }
+
+ DrawUtils.drawStringCV(gc, text, x, y, width, height);
+
+ if (resolvedText != null && resolvedText.length() > 0) {
+ Point size = gc.stringExtent(text);
+ x += size.x;
+ width -= size.x;
+
+ x += SAMPLE_MARGIN;
+ width -= SAMPLE_MARGIN;
+
+ if (width > 0) {
+ Color oldForeground = gc.getForeground();
+ try {
+ gc.setForeground(PropertyTable.COLOR_PROPERTY_FG_DEFAULT);
+ DrawUtils.drawStringCV(gc, '(' + resolvedText + ')', x, y, width, height);
+ } finally {
+ gc.setForeground(oldForeground);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected boolean setEditorText(Property property, String text) throws Exception {
+ Object oldValue = property.getValue();
+ String old = oldValue != null ? oldValue.toString() : null;
+
+ // If users enters a new id without specifying the @id/@+id prefix, insert it
+ boolean isId = isIdProperty(property);
+ if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) {
+ text = NEW_ID_PREFIX + text;
+ }
+
+ // Handle id refactoring: if you change an id, may want to update references too.
+ // Ask user.
+ if (isId && property instanceof XmlProperty
+ && old != null && !old.isEmpty()
+ && text != null && !text.isEmpty()
+ && !text.equals(old)) {
+ XmlProperty xmlProperty = (XmlProperty) property;
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS);
+ boolean performRefactor = false;
+ Shell shell = AdtPlugin.getShell();
+ if (refactorPref == null
+ || refactorPref.isEmpty()
+ || refactorPref.equals(MessageDialogWithToggle.PROMPT)) {
+ MessageDialogWithToggle dialog =
+ MessageDialogWithToggle.openYesNoCancelQuestion(
+ shell,
+ "Update References?",
+ "Update all references as well? " +
+ "This will update all XML references and Java R field references.",
+ "Do not show again",
+ false,
+ store,
+ AdtPrefs.PREFS_REFACTOR_IDS);
+ switch (dialog.getReturnCode()) {
+ case IDialogConstants.CANCEL_ID:
+ return false;
+ case IDialogConstants.YES_ID:
+ performRefactor = true;
+ break;
+ case IDialogConstants.NO_ID:
+ performRefactor = false;
+ break;
+ }
+ } else {
+ performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS);
+ }
+ if (performRefactor) {
+ CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor();
+ if (xmlEditor != null) {
+ IProject project = xmlEditor.getProject();
+ if (project != null && shell != null) {
+ RenameResourceWizard.renameResource(shell, project,
+ ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false);
+ }
+ }
+ }
+ }
+
+ property.setValue(text);
+
+ return true;
+ }
+
+ private static boolean isIdProperty(Property property) {
+ XmlProperty xmlProperty = (XmlProperty) property;
+ return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID);
+ }
+
+ private void openDialog(PropertyTable propertyTable, Property property) throws Exception {
+ XmlProperty xmlProperty = (XmlProperty) property;
+ IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo();
+
+ if (isIdProperty(property)) {
+ Object value = xmlProperty.getValue();
+ if (value != null && !value.toString().isEmpty()) {
+ GraphicalEditorPart editor = xmlProperty.getGraphicalEditor();
+ if (editor != null) {
+ LayoutCanvas canvas = editor.getCanvasControl();
+ SelectionManager manager = canvas.getSelectionManager();
+
+ NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode());
+ if (primary != null) {
+ RenameResult result = manager.performRename(primary, null);
+ if (result.isCanceled()) {
+ return;
+ } else if (!result.isUnavailable()) {
+ String name = result.getName();
+ String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name);
+ xmlProperty.setValue(id);
+ return;
+ }
+ }
+ }
+ }
+
+ // When editing the id attribute, don't offer a resource chooser: usually
+ // you want to enter a *new* id here
+ attributeInfo = null;
+ }
+
+ boolean referenceAllowed = false;
+ if (attributeInfo != null) {
+ EnumSet<Format> formats = attributeInfo.getFormats();
+ ResourceType type = null;
+ List<ResourceType> types = null;
+ if (formats.contains(Format.FLAG)) {
+ String[] flagValues = attributeInfo.getFlagValues();
+ if (flagValues != null) {
+ FlagXmlPropertyDialog dialog =
+ new FlagXmlPropertyDialog(propertyTable.getShell(),
+ "Select Flag Values", false /* radio */,
+ flagValues, xmlProperty);
+
+ dialog.open();
+ return;
+ }
+ } else if (formats.contains(Format.ENUM)) {
+ String[] enumValues = attributeInfo.getEnumValues();
+ if (enumValues != null) {
+ FlagXmlPropertyDialog dialog =
+ new FlagXmlPropertyDialog(propertyTable.getShell(),
+ "Select Enum Value", true /* radio */,
+ enumValues, xmlProperty);
+ dialog.open();
+ return;
+ }
+ } else {
+ for (Format format : formats) {
+ ResourceType t = format.getResourceType();
+ if (t != null) {
+ if (type != null) {
+ if (types == null) {
+ types = new ArrayList<ResourceType>();
+ types.add(type);
+ }
+ types.add(t);
+ }
+ type = t;
+ } else if (format == Format.REFERENCE) {
+ referenceAllowed = true;
+ }
+ }
+ }
+ if (types != null || referenceAllowed) {
+ // Multiple resource types (such as string *and* boolean):
+ // just use a reference chooser
+ GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
+ if (graphicalEditor != null) {
+ LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate();
+ IProject project = delegate.getEditor().getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceRepository projectRepository =
+ ResourceManager.getInstance().getProjectResources(project);
+ Shell shell = AdtPlugin.getShell();
+ ReferenceChooserDialog dlg = new ReferenceChooserDialog(
+ project,
+ projectRepository,
+ shell);
+ dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor));
+
+ String currentValue = (String) property.getValue();
+ dlg.setCurrentResource(currentValue);
+
+ if (dlg.open() == Window.OK) {
+ String resource = dlg.getCurrentResource();
+ if (resource != null) {
+ // Returns null for cancel, "" for clear and otherwise a new value
+ if (resource.length() > 0) {
+ property.setValue(resource);
+ } else {
+ property.setValue(null);
+ }
+ }
+ }
+
+ return;
+ }
+ }
+ } else if (type != null) {
+ // Single resource type: use a resource chooser
+ GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
+ if (graphicalEditor != null) {
+ String currentValue = (String) property.getValue();
+ // TODO: Add validator factory?
+ String resource = ResourceChooser.chooseResource(graphicalEditor,
+ type, currentValue, null /* validator */);
+ // Returns null for cancel, "" for clear and otherwise a new value
+ if (resource != null) {
+ if (resource.length() > 0) {
+ property.setValue(resource);
+ } else {
+ property.setValue(null);
+ }
+ }
+ }
+
+ return;
+ }
+ }
+
+ // Fallback: Just use a plain string editor
+ StringXmlPropertyDialog dialog =
+ new StringXmlPropertyDialog(propertyTable.getShell(), property);
+ if (dialog.open() == Window.OK) {
+ // TODO: Do I need to activate?
+ }
+ }
+
+ /** Qualified name for the per-project persistent property include-map */
+ private final static QualifiedName CACHE_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "property-images");//$NON-NLS-1$
+
+ @NonNull
+ private static Map<String, Image> getImageCache(@NonNull Property property) {
+ XmlProperty xmlProperty = (XmlProperty) property;
+ GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
+ IProject project = graphicalEditor.getProject();
+ try {
+ Map<String, Image> cache = (Map<String, Image>) project.getSessionProperty(CACHE_NAME);
+ if (cache == null) {
+ cache = Maps.newHashMap();
+ project.setSessionProperty(CACHE_NAME, cache);
+ }
+
+ return cache;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ return Maps.newHashMap();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java
new file mode 100644
index 000000000..306dd68c8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Convert Layout" menu item is invoked.
+ */
+public class ChangeLayoutAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ChangeLayoutRefactoring ref = new ChangeLayoutRefactoring(mFile, mDelegate,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ChangeLayoutWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Change Layout...", editorDelegate, ChangeLayoutAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java
new file mode 100644
index 000000000..c508b7e92
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class ChangeLayoutContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ChangeLayoutRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ChangeLayoutRefactoring.Descriptor) {
+ return ((ChangeLayoutRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
new file mode 100644
index 000000000..d8c85aab5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
@@ -0,0 +1,657 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.FQCN_RELATIVE_LAYOUT;
+import static com.android.SdkConstants.FQCN_TABLE_LAYOUT;
+import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.TABLE_ROW;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.MalformedTreeException;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Converts the selected layout into a layout of a different type.
+ */
+@SuppressWarnings("restriction") // XML model
+public class ChangeLayoutRefactoring extends VisualRefactoring {
+ private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+ private static final String KEY_FLATTEN = "flatten"; //$NON-NLS-1$
+
+ private String mTypeFqcn;
+ private String mInitializedAttributes;
+ private boolean mFlatten;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ ChangeLayoutRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mTypeFqcn = arguments.get(KEY_TYPE);
+ mFlatten = Boolean.parseBoolean(arguments.get(KEY_FLATTEN));
+ }
+
+ @VisibleForTesting
+ ChangeLayoutRefactoring(List<Element> selectedElements, LayoutEditorDelegate delegate) {
+ super(selectedElements, delegate);
+ }
+
+ public ChangeLayoutRefactoring(
+ IFile file,
+ LayoutEditorDelegate delegate,
+ ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, delegate, selection, treeSelection);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 2);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to convert");
+ return status;
+ }
+
+ if (mElements.size() != 1) {
+ status.addFatalError("Select precisely one layout to convert");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_TYPE, mTypeFqcn);
+ args.put(KEY_FLATTEN, Boolean.toString(mFlatten));
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Change Layout";
+ }
+
+ void setType(String typeFqcn) {
+ mTypeFqcn = typeFqcn;
+ }
+
+ void setInitializedAttributes(String initializedAttributes) {
+ mInitializedAttributes = initializedAttributes;
+ }
+
+ void setFlatten(boolean flatten) {
+ mFlatten = flatten;
+ }
+
+ @Override
+ protected List<Element> initElements() {
+ List<Element> elements = super.initElements();
+
+ // Don't convert a root GestureOverlayView; convert its child. This looks for
+ // gesture overlays, and if found, it generates a new child list where the gesture
+ // overlay children are replaced by their first element children
+ for (Element element : elements) {
+ String tagName = element.getTagName();
+ if (tagName.equals(GESTURE_OVERLAY_VIEW)
+ || tagName.equals(FQCN_GESTURE_OVERLAY_VIEW)) {
+ List<Element> replacement = new ArrayList<Element>(elements.size());
+ for (Element e : elements) {
+ tagName = e.getTagName();
+ if (tagName.equals(GESTURE_OVERLAY_VIEW)
+ || tagName.equals(FQCN_GESTURE_OVERLAY_VIEW)) {
+ NodeList children = e.getChildNodes();
+ Element first = null;
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ first = (Element) node;
+ break;
+ }
+ }
+ if (first != null) {
+ e = first;
+ }
+ }
+ replacement.add(e);
+ }
+ return replacement;
+ }
+ }
+
+ return elements;
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ String name = getViewClass(mTypeFqcn);
+
+ IFile file = mDelegate.getEditor().getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ if (file == null) {
+ return changes;
+ }
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setTextType(EXT_XML);
+ changes.add(change);
+
+ String text = getText(mSelectionStart, mSelectionEnd);
+ Element layout = getPrimaryElement();
+ String oldName = layout.getNodeName();
+ int open = text.indexOf(oldName);
+ int close = text.lastIndexOf(oldName);
+
+ if (open != -1 && close != -1) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength, name));
+ if (close != open) { // Gracefully handle <FooLayout/>
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + close, oldLength, name));
+ }
+ }
+
+ String oldId = getId(layout);
+ String newId = ensureIdMatchesType(layout, mTypeFqcn, rootEdit);
+ // Update any layout references to the old id with the new id
+ if (oldId != null && newId != null) {
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc,
+ mSelectionStart,
+ mSelectionEnd, oldId, newId);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ String oldType = getOldType();
+ String newType = mTypeFqcn;
+
+ if (newType.equals(FQCN_RELATIVE_LAYOUT)) {
+ if (oldType.equals(FQCN_LINEAR_LAYOUT) && !mFlatten) {
+ // Hand-coded conversion specifically tailored for linear to relative, provided
+ // there is no hierarchy flattening
+ // TODO: use the RelativeLayoutConversionHelper for this; it does a better job
+ // analyzing gravities etc.
+ convertLinearToRelative(rootEdit);
+ removeUndefinedAttrs(rootEdit, layout);
+ addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
+ } else {
+ // Generic conversion to relative - can also flatten the hierarchy
+ convertAnyToRelative(rootEdit, oldType, newType);
+ // This already handles removing undefined layout attributes -- right?
+ //removeUndefinedLayoutAttrs(rootEdit, layout);
+ }
+ } else if (newType.equals(FQCN_GRID_LAYOUT)) {
+ convertAnyToGridLayout(rootEdit);
+ // Layout attributes on children have already been removed as part of conversion
+ // during the flattening
+ removeUndefinedAttrs(rootEdit, layout, false /*removeLayoutAttrs*/);
+ } else if (oldType.equals(FQCN_RELATIVE_LAYOUT) && newType.equals(FQCN_LINEAR_LAYOUT)) {
+ convertRelativeToLinear(rootEdit);
+ removeUndefinedAttrs(rootEdit, layout);
+ addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
+ } else if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_TABLE_LAYOUT)) {
+ convertLinearToTable(rootEdit);
+ removeUndefinedAttrs(rootEdit, layout);
+ addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
+ } else {
+ convertGeneric(rootEdit, oldType, newType, layout);
+ }
+
+ if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) {
+ String namespace = getAndroidNamespacePrefix();
+ for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$
+ String[] nameValue = s.split("="); //$NON-NLS-1$
+ String attribute = nameValue[0];
+ String value = nameValue[1];
+ String prefix = null;
+ String namespaceUri = null;
+ if (attribute.startsWith(SdkConstants.ANDROID_NS_NAME_PREFIX)) {
+ prefix = namespace;
+ namespaceUri = ANDROID_URI;
+ attribute = attribute.substring(SdkConstants.ANDROID_NS_NAME_PREFIX.length());
+ }
+ setAttribute(rootEdit, layout, namespaceUri,
+ prefix, attribute, value);
+ }
+ }
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+ }
+ change.setEdit(rootEdit);
+
+ return changes;
+ }
+
+ /** Checks whether we need to add any missing attributes on the elements */
+ private void addMissingWrapContentAttributes(MultiTextEdit rootEdit, Element layout,
+ String oldType, String newType, Set<Element> skip) {
+ if (oldType.equals(FQCN_GRID_LAYOUT) && !newType.equals(FQCN_GRID_LAYOUT)) {
+ String namespace = getAndroidNamespacePrefix();
+
+ for (Element child : DomUtilities.getChildren(layout)) {
+ if (skip != null && skip.contains(child)) {
+ continue;
+ }
+
+ if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
+ setAttribute(rootEdit, child, ANDROID_URI,
+ namespace, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
+ }
+ if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
+ setAttribute(rootEdit, child, ANDROID_URI,
+ namespace, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
+ }
+ }
+ }
+ }
+
+ /** Hand coded conversion from a LinearLayout to a TableLayout */
+ private void convertLinearToTable(MultiTextEdit rootEdit) {
+ // This is pretty easy; just switch the root tag (already done by the initial generic
+ // conversion) and then convert all the children into <TableRow> elements.
+ // Finally, get rid of the orientation attribute, if any.
+ Element layout = getPrimaryElement();
+ removeOrientationAttribute(rootEdit, layout);
+
+ NodeList children = layout.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+ String text = getText(start, end);
+ String oldName = child.getNodeName();
+ if (oldName.equals(LINEAR_LAYOUT)) {
+ removeOrientationAttribute(rootEdit, child);
+ int open = text.indexOf(oldName);
+ int close = text.lastIndexOf(oldName);
+
+ if (open != -1 && close != -1) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength,
+ TABLE_ROW));
+ if (close != open) { // Gracefully handle <FooLayout/>
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + close,
+ oldLength, TABLE_ROW));
+ }
+ }
+ } // else: WRAP in TableLayout!
+ }
+ }
+ }
+ }
+
+ /** Hand coded conversion from a LinearLayout to a RelativeLayout */
+ private void convertLinearToRelative(MultiTextEdit rootEdit) {
+ // This can be done accurately.
+ Element layout = getPrimaryElement();
+ // Horizontal is the default, so if no value is specified it is horizontal.
+ boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
+ ATTR_ORIENTATION));
+
+ String attributePrefix = getAndroidNamespacePrefix();
+
+ // TODO: Consider gravity of each element
+ // TODO: Consider weight of each element
+ // Right now it simply makes a single attachment to keep the order.
+
+ if (isVertical) {
+ // Align each child to the bottom and left of its parent
+ NodeList children = layout.getChildNodes();
+ String prevId = null;
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ String id = ensureHasId(rootEdit, child, null);
+ if (prevId != null) {
+ setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
+ ATTR_LAYOUT_BELOW, prevId);
+ }
+ prevId = id;
+ }
+ }
+ } else {
+ // Align each child to the left
+ NodeList children = layout.getChildNodes();
+ boolean isBaselineAligned =
+ !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED));
+
+ String prevId = null;
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ String id = ensureHasId(rootEdit, child, null);
+ if (prevId != null) {
+ setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
+ ATTR_LAYOUT_TO_RIGHT_OF, prevId);
+ if (isBaselineAligned) {
+ setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
+ ATTR_LAYOUT_ALIGN_BASELINE, prevId);
+ }
+ }
+ prevId = id;
+ }
+ }
+ }
+ }
+
+ /** Strips out the android:orientation attribute from the given linear layout element */
+ private void removeOrientationAttribute(MultiTextEdit rootEdit, Element layout) {
+ assert layout.getTagName().equals(LINEAR_LAYOUT);
+ removeAttribute(rootEdit, layout, ANDROID_URI, ATTR_ORIENTATION);
+ }
+
+ /**
+ * Hand coded conversion from a RelativeLayout to a LinearLayout
+ *
+ * @param rootEdit the root multi text edit to add edits to
+ */
+ private void convertRelativeToLinear(MultiTextEdit rootEdit) {
+ // This is going to be lossy...
+ // TODO: Attempt to "order" the items based on their visual positions
+ // and insert them in that order in the LinearLayout.
+ // TODO: Possibly use nesting if necessary, by spatial subdivision,
+ // to accomplish roughly the same layout as the relative layout specifies.
+ }
+
+ /**
+ * Hand coded -generic- conversion from one layout to another. This is not going to be
+ * an accurate layout transformation; instead it simply migrates the layout attributes
+ * that are supported, and adds defaults for any new required layout attributes. In
+ * addition, it attempts to order the children visually based on where they fit in a
+ * rendering. (Unsupported layout attributes will be removed by the caller at the
+ * end.)
+ * <ul>
+ * <li>Try to handle nesting. Converting a *hierarchy* of layouts into a flatter
+ * layout for powerful layouts that support it, like RelativeLayout.
+ * <li>Try to do automatic "inference" about the layout. I can render it and look at
+ * the ViewInfo positions and sizes. I can render it multiple times, at different
+ * sizes, to infer "stretchiness" and "weight" properties of the children.
+ * <li>Try to do indirect transformations. E.g. if I can go from A to B, and B to C,
+ * then an attempt to go from A to C should perform conversions A to B and then B to
+ * C.
+ * </ul>
+ *
+ * @param rootEdit the root multi text edit to add edits to
+ * @param oldType the fully qualified class name of the layout type we are converting
+ * from
+ * @param newType the fully qualified class name of the layout type we are converting
+ * to
+ * @param layout the layout to be converted
+ */
+ private void convertGeneric(MultiTextEdit rootEdit, String oldType, String newType,
+ Element layout) {
+ // TODO: Add hooks for 3rd party conversions getting registered through the
+ // IViewRule interface.
+
+ // For now we simply go with the default behavior, which is to just strip the
+ // layout attributes that aren't supported.
+ removeUndefinedAttrs(rootEdit, layout);
+ addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
+ }
+
+ /**
+ * Removes all the unavailable attributes after a conversion, both on the
+ * layout element itself as well as the layout attributes of any of the
+ * children
+ */
+ private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout) {
+ removeUndefinedAttrs(rootEdit, layout, true /*removeLayoutAttrs*/);
+ }
+
+ private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout,
+ boolean removeLayoutAttrs) {
+ ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn);
+ if (descriptor == null) {
+ return;
+ }
+
+ if (removeLayoutAttrs) {
+ Set<String> defined = new HashSet<String>();
+ AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes();
+ for (AttributeDescriptor attribute : layoutAttributes) {
+ defined.add(attribute.getXmlLocalName());
+ }
+
+ NodeList children = layout.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+
+ List<Attr> attributes = findLayoutAttributes(child);
+ for (Attr attribute : attributes) {
+ String name = attribute.getLocalName();
+ if (!defined.contains(name)) {
+ // Remove it
+ try {
+ removeAttribute(rootEdit, child, attribute.getNamespaceURI(), name);
+ } catch (MalformedTreeException mte) {
+ // Sometimes refactoring has modified attribute; not removing
+ // it is non-fatal so just warn instead of letting refactoring
+ // operation abort
+ AdtPlugin.log(IStatus.WARNING,
+ "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$
+ "already modified during refactoring?", //$NON-NLS-1$
+ attribute.getLocalName());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Also remove the unavailable attributes (not layout attributes) on the
+ // converted element
+ Set<String> defined = new HashSet<String>();
+ AttributeDescriptor[] attributes = descriptor.getAttributes();
+ for (AttributeDescriptor attribute : attributes) {
+ defined.add(attribute.getXmlLocalName());
+ }
+
+ // Remove undefined attributes on the layout element itself
+ NamedNodeMap attributeMap = layout.getAttributes();
+ for (int i = 0, n = attributeMap.getLength(); i < n; i++) {
+ Node attributeNode = attributeMap.item(i);
+
+ String name = attributeNode.getLocalName();
+ if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
+ if (!defined.contains(name)) {
+ // Remove it
+ removeAttribute(rootEdit, layout, ANDROID_URI, name);
+ }
+ }
+ }
+ }
+
+ /** Hand coded conversion from any layout to a RelativeLayout */
+ private void convertAnyToRelative(MultiTextEdit rootEdit, String oldType, String newType) {
+ // To perform a conversion from any other layout type, including nested conversion,
+ Element layout = getPrimaryElement();
+ CanvasViewInfo rootView = mRootView;
+ if (rootView == null) {
+ LayoutCanvas canvas = mDelegate.getGraphicalEditor().getCanvasControl();
+ ViewHierarchy viewHierarchy = canvas.getViewHierarchy();
+ rootView = viewHierarchy.getRoot();
+ }
+
+ RelativeLayoutConversionHelper helper =
+ new RelativeLayoutConversionHelper(this, layout, mFlatten, rootEdit, rootView);
+ helper.convertToRelative();
+ List<Element> deletedElements = helper.getDeletedElements();
+ Set<Element> deleted = null;
+ if (deletedElements != null && deletedElements.size() > 0) {
+ deleted = new HashSet<Element>(deletedElements);
+ }
+ addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, deleted);
+ }
+
+ /** Hand coded conversion from any layout to a GridLayout */
+ private void convertAnyToGridLayout(MultiTextEdit rootEdit) {
+ // To perform a conversion from any other layout type, including nested conversion,
+ Element layout = getPrimaryElement();
+ CanvasViewInfo rootView = mRootView;
+ if (rootView == null) {
+ LayoutCanvas canvas = mDelegate.getGraphicalEditor().getCanvasControl();
+ ViewHierarchy viewHierarchy = canvas.getViewHierarchy();
+ rootView = viewHierarchy.getRoot();
+ }
+
+ GridLayoutConverter converter = new GridLayoutConverter(this, layout, mFlatten,
+ rootEdit, rootView);
+ converter.convertToGridLayout();
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.convert", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ChangeLayoutRefactoring(args);
+ }
+ }
+
+ String getOldType() {
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String oldType = primary.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ return oldType;
+ }
+
+ return null;
+ }
+
+ @VisibleForTesting
+ protected CanvasViewInfo mRootView;
+
+ @VisibleForTesting
+ public void setRootView(CanvasViewInfo rootView) {
+ mRootView = rootView;
+ }
+
+ @Override
+ VisualRefactoringWizard createWizard() {
+ return new ChangeLayoutWizard(this, mDelegate);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java
new file mode 100644
index 000000000..f5582712f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
+import static com.android.SdkConstants.FQCN_RELATIVE_LAYOUT;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.RELATIVE_LAYOUT;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+class ChangeLayoutWizard extends VisualRefactoringWizard {
+
+ public ChangeLayoutWizard(ChangeLayoutRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle("Change Layout");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ ChangeLayoutRefactoring ref = (ChangeLayoutRefactoring) getRefactoring();
+ String oldType = ref.getOldType();
+ addPage(new InputPage(mDelegate.getEditor().getProject(), oldType));
+ }
+
+ /** Wizard page which inputs parameters for the {@link ChangeLayoutRefactoring} operation */
+ private static class InputPage extends VisualRefactoringInputPage {
+ private final IProject mProject;
+ private final String mOldType;
+ private Combo mTypeCombo;
+ private Button mFlatten;
+ private List<Pair<String, ViewElementDescriptor>> mClassNames;
+
+ public InputPage(IProject project, String oldType) {
+ super("ChangeLayoutInputPage"); //$NON-NLS-1$
+ mProject = project;
+ mOldType = oldType;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label fromLabel = new Label(composite, SWT.NONE);
+ fromLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ String oldTypeBase = mOldType.substring(mOldType.lastIndexOf('.') + 1);
+ fromLabel.setText(String.format("Change from %1$s", oldTypeBase));
+
+ Label typeLabel = new Label(composite, SWT.NONE);
+ typeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ typeLabel.setText("New Layout Type:");
+
+ mTypeCombo = new Combo(composite, SWT.READ_ONLY);
+ mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ SelectionAdapter selectionListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ // Hierarchy flattening only works for relative layout (and any future
+ // layouts that can also support arbitrary layouts).
+ String text = mTypeCombo.getText();
+ mFlatten.setVisible(text.equals(RELATIVE_LAYOUT) || text.equals(GRID_LAYOUT));
+ }
+ };
+ mTypeCombo.addSelectionListener(selectionListener);
+ mTypeCombo.addSelectionListener(mSelectionValidateListener);
+
+ mFlatten = new Button(composite, SWT.CHECK);
+ mFlatten.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
+ false, false, 2, 1));
+ mFlatten.setText("Flatten hierarchy");
+ mFlatten.addSelectionListener(selectionListener);
+ // Should flattening be selected by default?
+ mFlatten.setSelection(true);
+ mFlatten.addSelectionListener(mSelectionValidateListener);
+
+ // We don't exclude RelativeLayout even if the current layout is RelativeLayout,
+ // in case you are trying to flatten the hierarchy for a hierarchy that has a
+ // RelativeLayout at the root.
+ Set<String> exclude = new HashSet<String>();
+ exclude.add(VIEW_INCLUDE);
+ exclude.add(VIEW_MERGE);
+ exclude.add(VIEW_FRAGMENT);
+ boolean oldIsRelativeLayout = mOldType.equals(FQCN_RELATIVE_LAYOUT);
+ boolean oldIsGridLayout = mOldType.equals(FQCN_GRID_LAYOUT);
+ if (oldIsRelativeLayout || oldIsGridLayout) {
+ exclude.add(mOldType);
+ }
+ mClassNames = WrapInWizard.addLayouts(mProject, mOldType, mTypeCombo, exclude, false);
+
+ boolean gridLayoutAvailable = false;
+ for (int i = 0; i < mTypeCombo.getItemCount(); i++) {
+ if (mTypeCombo.getItem(i).equals(GRID_LAYOUT)) {
+ gridLayoutAvailable = true;
+ break;
+ }
+ }
+
+ mTypeCombo.select(0);
+ // The default should be GridLayout (if available) and if not RelativeLayout,
+ // if available (and not the old Type)
+ if (gridLayoutAvailable && !oldIsGridLayout) {
+ for (int i = 0; i < mTypeCombo.getItemCount(); i++) {
+ if (mTypeCombo.getItem(i).equals(GRID_LAYOUT)) {
+ mTypeCombo.select(i);
+ break;
+ }
+ }
+ } else if (!oldIsRelativeLayout) {
+ for (int i = 0; i < mTypeCombo.getItemCount(); i++) {
+ if (mTypeCombo.getItem(i).equals(RELATIVE_LAYOUT)) {
+ mTypeCombo.select(i);
+ break;
+ }
+ }
+ }
+ mFlatten.setVisible(mTypeCombo.getText().equals(RELATIVE_LAYOUT)
+ || mTypeCombo.getText().equals(GRID_LAYOUT));
+
+ setControl(composite);
+ validatePage();
+ }
+
+ @Override
+ protected boolean validatePage() {
+ boolean ok = true;
+
+ int selectionIndex = mTypeCombo.getSelectionIndex();
+ String type = selectionIndex != -1 ? mClassNames.get(selectionIndex).getFirst() : null;
+ if (type == null) {
+ setErrorMessage("Select a layout type");
+ ok = false; // The user has chosen a separator
+ } else {
+ setErrorMessage(null);
+
+ // Record state
+ ChangeLayoutRefactoring refactoring =
+ (ChangeLayoutRefactoring) getRefactoring();
+ refactoring.setType(type);
+ refactoring.setFlatten(mFlatten.getSelection());
+
+ ViewElementDescriptor descriptor = mClassNames.get(selectionIndex).getSecond();
+ if (descriptor instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor paletteDescriptor =
+ (PaletteMetadataDescriptor) descriptor;
+ String initializedAttributes = paletteDescriptor.getInitializedAttributes();
+ if (initializedAttributes != null && initializedAttributes.length() > 0) {
+ refactoring.setInitializedAttributes(initializedAttributes);
+ }
+ } else {
+ refactoring.setInitializedAttributes(null);
+ }
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java
new file mode 100644
index 000000000..fa14e5222
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Change View Type" menu item is invoked.
+ */
+public class ChangeViewAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ChangeViewRefactoring ref = new ChangeViewRefactoring(mFile, mDelegate,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ChangeViewWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Change Widget Type...", editorDelegate, ChangeViewAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java
new file mode 100644
index 000000000..7705ed808
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class ChangeViewContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ChangeViewRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ChangeViewRefactoring.Descriptor) {
+ return ((ChangeViewRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java
new file mode 100644
index 000000000..73f5eb149
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.document.ElementImpl;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Changes the type of the given widgets to the given target type
+ * and updates the attributes if necessary
+ */
+@SuppressWarnings("restriction") // XML model
+public class ChangeViewRefactoring extends VisualRefactoring {
+ private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+ private String mTypeFqcn;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ ChangeViewRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mTypeFqcn = arguments.get(KEY_TYPE);
+ }
+
+ public ChangeViewRefactoring(
+ IFile file,
+ LayoutEditorDelegate delegate,
+ ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, delegate, selection, treeSelection);
+ }
+
+ @VisibleForTesting
+ ChangeViewRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
+ super(selectedElements, editor);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to convert");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+ }
+
+ // Ensures that we have a valid DOM model:
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to convert");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_TYPE, mTypeFqcn);
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Change Widget Type";
+ }
+
+ void setType(String typeFqcn) {
+ mTypeFqcn = typeFqcn;
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ String name = getViewClass(mTypeFqcn);
+
+ IFile file = mDelegate.getEditor().getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ if (file == null) {
+ return changes;
+ }
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setEdit(rootEdit);
+ change.setTextType(EXT_XML);
+ changes.add(change);
+
+ for (Element element : getElements()) {
+ IndexedRegion region = getRegion(element);
+ String text = getText(region.getStartOffset(), region.getEndOffset());
+ String oldName = element.getNodeName();
+ int open = text.indexOf(oldName);
+ int close = text.lastIndexOf(oldName);
+ if (element instanceof ElementImpl && ((ElementImpl) element).isEmptyTag()) {
+ close = -1;
+ }
+
+ if (open != -1) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + open,
+ oldLength, name));
+ }
+ if (close != -1 && close != open) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + close, oldLength,
+ name));
+ }
+
+ // Change tag type
+ String oldId = getId(element);
+ String newId = ensureIdMatchesType(element, mTypeFqcn, rootEdit);
+ // Update any layout references to the old id with the new id
+ if (oldId != null && newId != null) {
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ IndexedRegion range = getRegion(element);
+ int skipStart = range.getStartOffset();
+ int skipEnd = range.getEndOffset();
+ List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc,
+ skipStart, skipEnd,
+ oldId, newId);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ // Strip out attributes that no longer make sense
+ removeUndefinedAttrs(rootEdit, element);
+ }
+
+ return changes;
+ }
+
+ /** Removes all the unused attributes after a conversion */
+ private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element element) {
+ ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn);
+ if (descriptor == null) {
+ return;
+ }
+
+ Set<String> defined = new HashSet<String>();
+ AttributeDescriptor[] layoutAttributes = descriptor.getAttributes();
+ for (AttributeDescriptor attribute : layoutAttributes) {
+ defined.add(attribute.getXmlLocalName());
+ }
+
+ List<Attr> attributes = findAttributes(element);
+ for (Attr attribute : attributes) {
+ String name = attribute.getLocalName();
+ if (!defined.contains(name)) {
+ // Remove it
+ removeAttribute(rootEdit, element, attribute.getNamespaceURI(), name);
+ }
+ }
+
+ // Set text attribute if it's defined
+ if (defined.contains(ATTR_TEXT) && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
+ setAttribute(rootEdit, element, ANDROID_URI, getAndroidNamespacePrefix(),
+ ATTR_TEXT, descriptor.getUiName());
+ }
+ }
+
+ protected List<Attr> findAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String name = attributeNode.getLocalName();
+ if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ List<String> getOldTypes() {
+ List<String> types = new ArrayList<String>();
+ for (Element primary : getElements()) {
+ String oldType = primary.getTagName();
+ if (oldType.indexOf('.') == -1
+ && !oldType.equals(VIEW_INCLUDE) && !oldType.equals(VIEW_FRAGMENT)) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ types.add(oldType);
+ }
+
+ return types;
+ }
+
+ @Override
+ VisualRefactoringWizard createWizard() {
+ return new ChangeViewWizard(this, mDelegate);
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.changeview", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ChangeViewRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java
new file mode 100644
index 000000000..0ac7106b3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.REQUEST_FOCUS;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CustomViewFinder;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class ChangeViewWizard extends VisualRefactoringWizard {
+ private static final String SEPARATOR_LABEL =
+ "----------------------------------------"; //$NON-NLS-1$
+
+ public ChangeViewWizard(ChangeViewRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle("Change Widget Type");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ ChangeViewRefactoring ref = (ChangeViewRefactoring) getRefactoring();
+ List<String> oldTypes = ref.getOldTypes();
+ String oldType = null;
+ for (String type : oldTypes) {
+ if (oldType == null) {
+ oldType = type;
+ } else if (!oldType.equals(type)) {
+ // If the types differ, don't offer related categories
+ oldType = null;
+ break;
+ }
+ }
+ addPage(new InputPage(mDelegate.getEditor().getProject(), oldType));
+ }
+
+ /** Wizard page which inputs parameters for the {@link ChangeViewRefactoring} operation */
+ private static class InputPage extends VisualRefactoringInputPage {
+ private final IProject mProject;
+ private Combo mTypeCombo;
+ private final String mOldType;
+ private List<String> mClassNames;
+
+ public InputPage(IProject project, String oldType) {
+ super("ChangeViewInputPage"); //$NON-NLS-1$
+ mProject = project;
+ mOldType = oldType;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label typeLabel = new Label(composite, SWT.NONE);
+ typeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ typeLabel.setText("New Widget Type:");
+
+ mTypeCombo = new Combo(composite, SWT.READ_ONLY);
+ mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mTypeCombo.addSelectionListener(mSelectionValidateListener);
+
+ mClassNames = getWidgetTypes(mOldType, mTypeCombo);
+ mTypeCombo.select(0);
+
+ setControl(composite);
+ validatePage();
+
+ mTypeCombo.setFocus();
+ }
+
+ private List<String> getWidgetTypes(String oldType, Combo combo) {
+ List<String> classNames = new ArrayList<String>();
+
+ // Populate type combo
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mProject);
+ if (target != null) {
+ // Try to pick "related" widgets to the one you have selected.
+ // For example, for an AnalogClock, display DigitalClock first.
+ // For a Text, offer EditText, AutoComplete, etc.
+ if (oldType != null) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ List<String> relatedTo = repository.getRelatedTo(oldType);
+ if (relatedTo.size() > 0) {
+ for (String className : relatedTo) {
+ String base = className.substring(className.lastIndexOf('.') + 1);
+ combo.add(base);
+ classNames.add(className);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+ }
+
+ Pair<List<String>,List<String>> result =
+ CustomViewFinder.findViews(mProject, false);
+ List<String> customViews = result.getFirst();
+ List<String> thirdPartyViews = result.getSecond();
+ if (customViews.size() > 0) {
+ for (String view : customViews) {
+ combo.add(view);
+ classNames.add(view);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ if (thirdPartyViews.size() > 0) {
+ for (String view : thirdPartyViews) {
+ combo.add(view);
+ classNames.add(view);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ AndroidTargetData targetData = currentSdk.getTargetData(target);
+ if (targetData != null) {
+ // Now add ALL known layout descriptors in case the user has
+ // a special case
+ List<ViewElementDescriptor> descriptors =
+ targetData.getLayoutDescriptors().getViewDescriptors();
+ for (ViewElementDescriptor d : descriptors) {
+ String className = d.getFullClassName();
+ if (className.equals(VIEW_INCLUDE)
+ || className.equals(VIEW_FRAGMENT)
+ || className.equals(REQUEST_FOCUS)) {
+ continue;
+ }
+ combo.add(d.getUiName());
+ classNames.add(className);
+
+ }
+ }
+ }
+ } else {
+ combo.add("SDK not initialized");
+ classNames.add(null);
+ }
+
+ return classNames;
+ }
+
+ @Override
+ protected boolean validatePage() {
+ boolean ok = true;
+ int selectionIndex = mTypeCombo.getSelectionIndex();
+ String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
+ if (type == null) {
+ setErrorMessage("Select a widget type to convert to");
+ ok = false; // The user has chosen a separator
+ } else {
+ setErrorMessage(null);
+ }
+
+ // Record state
+ ChangeViewRefactoring refactoring =
+ (ChangeViewRefactoring) getRefactoring();
+ refactoring.setType(type);
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java
new file mode 100644
index 000000000..6f96fe489
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Extract as Include" menu item is invoked.
+ */
+public class ExtractIncludeAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ExtractIncludeRefactoring ref = new ExtractIncludeRefactoring(mFile, mDelegate,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ExtractIncludeWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Extract Include...", editorDelegate, ExtractIncludeAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java
new file mode 100644
index 000000000..5903812ea
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class ExtractIncludeContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ExtractIncludeRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ExtractIncludeRefactoring.Descriptor) {
+ return ((ExtractIncludeRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
new file mode 100644
index 000000000..f58ac5501
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_RES;
+import static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_LAYOUT;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.SdkConstants.XMLNS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+import static com.android.resources.ResourceType.LAYOUT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.utils.XmlUtils;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Extracts the selection and writes it out as a separate layout file, then adds an
+ * include to that new layout file. Interactively asks the user for a new name for the
+ * layout.
+ */
+@SuppressWarnings("restriction") // XML model
+public class ExtractIncludeRefactoring extends VisualRefactoring {
+ private static final String KEY_NAME = "name"; //$NON-NLS-1$
+ private static final String KEY_OCCURRENCES = "all-occurrences"; //$NON-NLS-1$
+ private String mLayoutName;
+ private boolean mReplaceOccurrences;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ ExtractIncludeRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mLayoutName = arguments.get(KEY_NAME);
+ mReplaceOccurrences = Boolean.parseBoolean(arguments.get(KEY_OCCURRENCES));
+ }
+
+ public ExtractIncludeRefactoring(
+ IFile file,
+ LayoutEditorDelegate delegate,
+ ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, delegate, selection, treeSelection);
+ }
+
+ @VisibleForTesting
+ ExtractIncludeRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
+ super(selectedElements, editor);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to extract");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ // TODO - don't do this if we based the selection on text. In this case,
+ // make sure we're -balanced-.
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+
+ if (!validateNotRoot(infos, status)) {
+ return status;
+ }
+
+ // Disable if you've selected a single include tag
+ if (infos.size() == 1) {
+ UiViewElementNode uiNode = infos.get(0).getUiViewNode();
+ if (uiNode != null) {
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode.getLocalName().equals(VIEW_INCLUDE)) {
+ status.addWarning("No point in refactoring a single include tag");
+ }
+ }
+ }
+
+ // Enforce that the selection is -contiguous-
+ if (!validateContiguous(infos, status)) {
+ return status;
+ }
+ }
+
+ // This also ensures that we have a valid DOM model:
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to extract");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_NAME, mLayoutName);
+ args.put(KEY_OCCURRENCES, Boolean.toString(mReplaceOccurrences));
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Extract as Include";
+ }
+
+ void setLayoutName(String layoutName) {
+ mLayoutName = layoutName;
+ }
+
+ void setReplaceOccurrences(boolean selection) {
+ mReplaceOccurrences = selection;
+ }
+
+ // ---- Actual implementation of Extract as Include modification computation ----
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ String extractedText = getExtractedText();
+
+ String namespaceDeclarations = computeNamespaceDeclarations();
+
+ // Insert namespace:
+ extractedText = insertNamespace(extractedText, namespaceDeclarations);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
+ sb.append(extractedText);
+ sb.append('\n');
+
+ List<Change> changes = new ArrayList<Change>();
+
+ String newFileName = mLayoutName + DOT_XML;
+ IProject project = mDelegate.getEditor().getProject();
+ IFile sourceFile = mDelegate.getEditor().getInputFile();
+ if (sourceFile == null) {
+ return changes;
+ }
+
+ // Replace extracted elements by <include> tag
+ handleIncludingFile(changes, sourceFile, mSelectionStart, mSelectionEnd,
+ getDomDocument(), getPrimaryElement());
+
+ // Also extract in other variations of the same file (landscape/portrait, etc)
+ boolean haveVariations = false;
+ if (mReplaceOccurrences) {
+ List<IFile> layouts = getOtherLayouts(sourceFile);
+ for (IFile file : layouts) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ // We could enhance this with a SubMonitor to make the progress bar move as
+ // well.
+ monitor.subTask(String.format("Looking for duplicates in %1$s",
+ file.getProjectRelativePath()));
+ if (monitor.isCanceled()) {
+ throw new OperationCanceledException();
+ }
+
+ try {
+ model = modelManager.getModelForRead(file);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ IDOMDocument otherDocument = domModel.getDocument();
+ List<Element> otherElements = new ArrayList<Element>();
+ Element otherPrimary = null;
+
+ for (Element element : getElements()) {
+ Element other = DomUtilities.findCorresponding(element,
+ otherDocument);
+ if (other != null) {
+ // See if the structure is similar to what we have in this
+ // document
+ if (DomUtilities.isEquivalent(element, other)) {
+ otherElements.add(other);
+ if (element == getPrimaryElement()) {
+ otherPrimary = other;
+ }
+ }
+ }
+ }
+
+ // Only perform extract in the other file if we find a match for
+ // ALL of elements being extracted, and if they too are contiguous
+ if (otherElements.size() == getElements().size() &&
+ DomUtilities.isContiguous(otherElements)) {
+ // Find the range
+ int begin = Integer.MAX_VALUE;
+ int end = Integer.MIN_VALUE;
+ for (Element element : otherElements) {
+ // Yes!! Extract this one as well!
+ IndexedRegion region = getRegion(element);
+ end = Math.max(end, region.getEndOffset());
+ begin = Math.min(begin, region.getStartOffset());
+ }
+ handleIncludingFile(changes, file, begin,
+ end, otherDocument, otherPrimary);
+ haveVariations = true;
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+ }
+ }
+
+ // Add change to create the new file
+ IContainer parent = sourceFile.getParent();
+ if (haveVariations) {
+ // If we're extracting from multiple configuration folders, then we need to
+ // place the extracted include in the base layout folder (if not it goes next to
+ // the including file)
+ parent = mProject.getFolder(FD_RES).getFolder(FD_RES_LAYOUT);
+ }
+ IPath parentPath = parent.getProjectRelativePath();
+ final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName));
+ TextFileChange addFile = new TextFileChange("Create new separate layout", file);
+ addFile.setTextType(EXT_XML);
+ changes.add(addFile);
+
+ String newFile = sb.toString();
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ newFile = EclipseXmlPrettyPrinter.prettyPrint(newFile,
+ EclipseXmlFormatPreferences.create(), XmlFormatStyle.LAYOUT,
+ null /*lineSeparator*/);
+ }
+ addFile.setEdit(new InsertEdit(0, newFile));
+
+ Change finishHook = createFinishHook(file);
+ changes.add(finishHook);
+
+ return changes;
+ }
+
+ private void handleIncludingFile(List<Change> changes,
+ IFile sourceFile, int begin, int end, Document document, Element primary) {
+ TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setTextType(EXT_XML);
+ changes.add(change);
+
+ String referenceId = getReferenceId();
+ // Replace existing elements in the source file and insert <include>
+ String androidNsPrefix = getAndroidNamespacePrefix(document);
+ String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId);
+ int length = end - begin;
+ ReplaceEdit replace = new ReplaceEdit(begin, length, include);
+ rootEdit.addChild(replace);
+
+ // Update any layout references to the old id with the new id
+ if (referenceId != null && primary != null) {
+ String rootId = getId(primary);
+ IStructuredModel model = null;
+ try {
+ model = StructuredModelManager.getModelManager().getModelForRead(sourceFile);
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null && rootId != null) {
+ List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin,
+ end, rootId, referenceId);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ MultiTextEdit formatted = reformat(doc.get(), rootEdit,
+ XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+ }
+
+ change.setEdit(rootEdit);
+ }
+
+ /**
+ * Returns a list of all the other layouts (in all configurations) in the project other
+ * than the given source layout where the refactoring was initiated. Never null.
+ */
+ private List<IFile> getOtherLayouts(IFile sourceFile) {
+ List<IFile> layouts = new ArrayList<IFile>(100);
+ IPath sourcePath = sourceFile.getProjectRelativePath();
+ IFolder resources = mProject.getFolder(FD_RESOURCES);
+ try {
+ for (IResource folder : resources.members()) {
+ if (folder.getName().startsWith(FD_RES_LAYOUT) &&
+ folder instanceof IFolder) {
+ IFolder layoutFolder = (IFolder) folder;
+ for (IResource file : layoutFolder.members()) {
+ if (file.getName().endsWith(EXT_XML)
+ && file instanceof IFile) {
+ if (!file.getProjectRelativePath().equals(sourcePath)) {
+ layouts.add((IFile) file);
+ }
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return layouts;
+ }
+
+ String getInitialName() {
+ String defaultName = ""; //$NON-NLS-1$
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
+ // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
+ if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) {
+ // Use everything following the id/, and make it lowercase since that is
+ // the convention for layouts (and use Locale.US to ensure that "Image" becomes
+ // "image" etc)
+ defaultName = id.substring(id.indexOf('/') + 1).toLowerCase(Locale.US);
+
+ IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT);
+
+ if (validator.isValid(defaultName) != null) { // Already exists?
+ defaultName = ""; //$NON-NLS-1$
+ }
+ }
+ }
+
+ return defaultName;
+ }
+
+ IFile getSourceFile() {
+ return mFile;
+ }
+
+ private Change createFinishHook(final IFile file) {
+ return new NullChange("Open extracted layout and refresh resources") {
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ Display display = AdtPlugin.getDisplay();
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ openFile(file);
+ mDelegate.getGraphicalEditor().refreshProjectResources();
+ // Save file to trigger include finder scanning (as well as making
+ // the
+ // actual show-include feature work since it relies on reading
+ // files from
+ // disk, not a live buffer)
+ IWorkbenchPage page = mDelegate.getEditor().getEditorSite().getPage();
+ page.saveEditor(mDelegate.getEditor(), false);
+ }
+ });
+
+ // Not undoable: just return null instead of an undo-change.
+ return null;
+ }
+ };
+ }
+
+ private String computeNamespaceDeclarations() {
+ String androidNsPrefix = null;
+ String namespaceDeclarations = null;
+
+ StringBuilder sb = new StringBuilder();
+ List<Attr> attributeNodes = findNamespaceAttributes();
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ sb.append(' ');
+ String name = attributeNode.getNodeName();
+ sb.append(name);
+ sb.append('=').append('"');
+
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ androidNsPrefix = name;
+ if (androidNsPrefix.startsWith(XMLNS_PREFIX)) {
+ androidNsPrefix = androidNsPrefix.substring(XMLNS_PREFIX.length());
+ }
+ }
+ sb.append(XmlUtils.toXmlAttributeValue(value));
+ sb.append('"');
+ }
+ }
+ namespaceDeclarations = sb.toString();
+
+ if (androidNsPrefix == null) {
+ androidNsPrefix = ANDROID_NS_NAME;
+ }
+
+ if (namespaceDeclarations.length() == 0) {
+ sb.setLength(0);
+ sb.append(' ');
+ sb.append(XMLNS_PREFIX);
+ sb.append(androidNsPrefix);
+ sb.append('=').append('"');
+ sb.append(ANDROID_URI);
+ sb.append('"');
+ namespaceDeclarations = sb.toString();
+ }
+
+ return namespaceDeclarations;
+ }
+
+ /** Returns the id to be used for the include tag itself (may be null) */
+ private String getReferenceId() {
+ String rootId = getRootId();
+ if (rootId != null) {
+ return rootId + "_ref";
+ }
+
+ return null;
+ }
+
+ /**
+ * Compute the actual {@code <include>} string to be inserted in place of the old
+ * selection
+ */
+ private static String computeIncludeString(Element primaryNode, String newName,
+ String androidNsPrefix, String referenceId) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<include layout=\"@layout/"); //$NON-NLS-1$
+ sb.append(newName);
+ sb.append('"');
+ sb.append(' ');
+
+ // Create new id for the include itself
+ if (referenceId != null) {
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(ATTR_ID);
+ sb.append('=').append('"');
+ sb.append(referenceId);
+ sb.append('"').append(' ');
+ }
+
+ // Add id string, unless it's a <merge>, since we may need to adjust any layout
+ // references to apply to the <include> tag instead
+
+ // I should move all the layout_ attributes as well
+ // I also need to duplicate and modify the id and then replace
+ // everything else in the file with this new id...
+
+ // HACK: see issue 13494: We must duplicate the width/height attributes on the
+ // <include> statement for designtime rendering only
+ String width = null;
+ String height = null;
+ if (primaryNode == null) {
+ // Multiple selection - in that case we will be creating an outer <merge>
+ // so we need to set our own width/height on it
+ width = height = VALUE_WRAP_CONTENT;
+ } else {
+ if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
+ width = VALUE_WRAP_CONTENT;
+ } else {
+ width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ }
+ if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
+ height = VALUE_WRAP_CONTENT;
+ } else {
+ height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ }
+ }
+ if (width != null) {
+ sb.append(' ');
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(ATTR_LAYOUT_WIDTH);
+ sb.append('=').append('"');
+ sb.append(XmlUtils.toXmlAttributeValue(width));
+ sb.append('"');
+ }
+ if (height != null) {
+ sb.append(' ');
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(ATTR_LAYOUT_HEIGHT);
+ sb.append('=').append('"');
+ sb.append(XmlUtils.toXmlAttributeValue(height));
+ sb.append('"');
+ }
+
+ // Duplicate all the other layout attributes as well
+ if (primaryNode != null) {
+ NamedNodeMap attributes = primaryNode.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attr = attributes.item(i);
+ String name = attr.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attr.getNamespaceURI())) {
+ if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Already handled
+ continue;
+ }
+
+ sb.append(' ');
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(name);
+ sb.append('=').append('"');
+ sb.append(XmlUtils.toXmlAttributeValue(attr.getNodeValue()));
+ sb.append('"');
+ }
+ }
+ }
+
+ sb.append("/>");
+ return sb.toString();
+ }
+
+ /** Return the text in the document in the range start to end */
+ private String getExtractedText() {
+ String xml = getText(mSelectionStart, mSelectionEnd);
+ Element primaryNode = getPrimaryElement();
+ xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml);
+ xml = dedent(xml);
+
+ // Wrap siblings in <merge>?
+ if (primaryNode == null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<merge>\n"); //$NON-NLS-1$
+ // indent an extra level
+ for (String line : xml.split("\n")) { //$NON-NLS-1$
+ sb.append(" "); //$NON-NLS-1$
+ sb.append(line).append('\n');
+ }
+ sb.append("</merge>\n"); //$NON-NLS-1$
+ xml = sb.toString();
+ }
+
+ return xml;
+ }
+
+ @Override
+ VisualRefactoringWizard createWizard() {
+ return new ExtractIncludeWizard(this, mDelegate);
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.extract.include", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ExtractIncludeRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java
new file mode 100644
index 000000000..f3ac3f1b3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+class ExtractIncludeWizard extends VisualRefactoringWizard {
+ public ExtractIncludeWizard(ExtractIncludeRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle(ref.getName());
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ ExtractIncludeRefactoring ref = (ExtractIncludeRefactoring) getRefactoring();
+ String initialName = ref.getInitialName();
+ IFile sourceFile = ref.getSourceFile();
+ addPage(new InputPage(mDelegate.getEditor().getProject(), sourceFile, initialName));
+ }
+
+ /** Wizard page which inputs parameters for the {@link ExtractIncludeRefactoring} operation */
+ private static class InputPage extends VisualRefactoringInputPage {
+ private final IProject mProject;
+ private final IFile mSourceFile;
+ private final String mSuggestedName;
+ private Text mNameText;
+ private Button mReplaceAllOccurrences;
+
+ public InputPage(IProject project, IFile sourceFile, String suggestedName) {
+ super("ExtractIncludeInputPage");
+ mProject = project;
+ mSourceFile = sourceFile;
+ mSuggestedName = suggestedName;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label nameLabel = new Label(composite, SWT.NONE);
+ nameLabel.setText("New Layout Name:");
+ nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+
+ mNameText = new Text(composite, SWT.BORDER);
+ mNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mNameText.addModifyListener(mModifyValidateListener);
+
+ mReplaceAllOccurrences = new Button(composite, SWT.CHECK);
+ mReplaceAllOccurrences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
+ false, false, 2, 1));
+ mReplaceAllOccurrences.setText(
+ "Replace occurrences in all layouts with include to new layout");
+ mReplaceAllOccurrences.setEnabled(true);
+ mReplaceAllOccurrences.setSelection(true);
+ mReplaceAllOccurrences.addSelectionListener(mSelectionValidateListener);
+
+ // Initialize UI:
+ if (mSuggestedName != null) {
+ mNameText.setText(mSuggestedName);
+ }
+
+ setControl(composite);
+ validatePage();
+ }
+
+ @Override
+ protected boolean validatePage() {
+ boolean ok = true;
+
+ String text = mNameText.getText().trim();
+
+ if (text.length() == 0) {
+ setErrorMessage("Provide a name for the new layout");
+ ok = false;
+ } else {
+ ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
+ ResourceType.LAYOUT);
+ String message = validator.isValid(text);
+ if (message != null) {
+ setErrorMessage(message);
+ ok = false;
+ }
+ }
+
+ if (ok) {
+ setErrorMessage(null);
+
+ // Record state
+ ExtractIncludeRefactoring refactoring =
+ (ExtractIncludeRefactoring) getRefactoring();
+ refactoring.setLayoutName(text);
+ refactoring.setReplaceOccurrences(mReplaceAllOccurrences.getSelection());
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleAction.java
new file mode 100644
index 000000000..4a498637d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleAction.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Extract Style" menu item is invoked.
+ */
+public class ExtractStyleAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ExtractStyleRefactoring ref = new ExtractStyleRefactoring(mFile, mDelegate,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ExtractStyleWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Extract Style...", editorDelegate, ExtractStyleAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleContribution.java
new file mode 100644
index 000000000..95fbdbc43
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleContribution.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class ExtractStyleContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ExtractStyleRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ExtractStyleRefactoring.Descriptor) {
+ return ((ExtractStyleRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java
new file mode 100644
index 000000000..9b1770d82
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java
@@ -0,0 +1,579 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_HINT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_ON_CLICK;
+import static com.android.SdkConstants.ATTR_PARENT;
+import static com.android.SdkConstants.ATTR_SRC;
+import static com.android.SdkConstants.ATTR_STYLE;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_VALUES;
+import static com.android.SdkConstants.PREFIX_ANDROID;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.REFERENCE_STYLE;
+import static com.android.SdkConstants.TAG_ITEM;
+import static com.android.SdkConstants.TAG_RESOURCES;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Extracts the selection and writes it out as a separate layout file, then adds an
+ * include to that new layout file. Interactively asks the user for a new name for the
+ * layout.
+ * <p>
+ * Remaining work to do / Possible enhancements:
+ * <ul>
+ * <li>Optionally look in other files in the project and attempt to set style attributes
+ * in other cases where the style attributes match?
+ * <li>If the elements we are extracting from already contain a style attribute, set that
+ * style as the parent style of the current style?
+ * <li>Add a parent-style picker to the wizard (initialized with the above if applicable)
+ * <li>Pick up indentation settings from the XML module
+ * <li>Integrate with themes somehow -- make an option to have the extracted style go into
+ * the theme instead
+ * </ul>
+ */
+@SuppressWarnings("restriction") // XML model
+public class ExtractStyleRefactoring extends VisualRefactoring {
+ private static final String KEY_NAME = "name"; //$NON-NLS-1$
+ private static final String KEY_REMOVE_EXTRACTED = "removeextracted"; //$NON-NLS-1$
+ private static final String KEY_REMOVE_ALL = "removeall"; //$NON-NLS-1$
+ private static final String KEY_APPLY_STYLE = "applystyle"; //$NON-NLS-1$
+ private static final String KEY_PARENT = "parent"; //$NON-NLS-1$
+ private String mStyleName;
+ /** The name of the file in res/values/ that the style will be added to. Normally
+ * res/values/styles.xml - but unit tests pick other names */
+ private String mStyleFileName = "styles.xml";
+ /** Set a style reference on the extracted elements? */
+ private boolean mApplyStyle;
+ /** Remove the attributes that were extracted? */
+ private boolean mRemoveExtracted;
+ /** List of attributes chosen by the user to be extracted */
+ private List<Attr> mChosenAttributes = new ArrayList<Attr>();
+ /** Remove all attributes that match the extracted attributes names, regardless of value */
+ private boolean mRemoveAll;
+ /** The parent style to extend */
+ private String mParent;
+ /** The full list of available attributes in the refactoring */
+ private Map<String, List<Attr>> mAvailableAttributes;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ ExtractStyleRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mStyleName = arguments.get(KEY_NAME);
+ mRemoveExtracted = Boolean.parseBoolean(arguments.get(KEY_REMOVE_EXTRACTED));
+ mRemoveAll = Boolean.parseBoolean(arguments.get(KEY_REMOVE_ALL));
+ mApplyStyle = Boolean.parseBoolean(arguments.get(KEY_APPLY_STYLE));
+ mParent = arguments.get(KEY_PARENT);
+ if (mParent != null && mParent.length() == 0) {
+ mParent = null;
+ }
+ }
+
+ public ExtractStyleRefactoring(
+ IFile file,
+ LayoutEditorDelegate delegate,
+ ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, delegate, selection, treeSelection);
+ }
+
+ @VisibleForTesting
+ ExtractStyleRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
+ super(selectedElements, editor);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to extract");
+ return status;
+ }
+
+ // This also ensures that we have a valid DOM model:
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to extract");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_NAME, mStyleName);
+ args.put(KEY_REMOVE_EXTRACTED, Boolean.toString(mRemoveExtracted));
+ args.put(KEY_REMOVE_ALL, Boolean.toString(mRemoveAll));
+ args.put(KEY_APPLY_STYLE, Boolean.toString(mApplyStyle));
+ args.put(KEY_PARENT, mParent != null ? mParent : "");
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Extract Style";
+ }
+
+ void setStyleName(String styleName) {
+ mStyleName = styleName;
+ }
+
+ void setStyleFileName(String styleFileName) {
+ mStyleFileName = styleFileName;
+ }
+
+ void setChosenAttributes(List<Attr> attributes) {
+ mChosenAttributes = attributes;
+ }
+
+ void setRemoveExtracted(boolean removeExtracted) {
+ mRemoveExtracted = removeExtracted;
+ }
+
+ void setApplyStyle(boolean applyStyle) {
+ mApplyStyle = applyStyle;
+ }
+
+ void setRemoveAll(boolean removeAll) {
+ mRemoveAll = removeAll;
+ }
+
+ void setParent(String parent) {
+ mParent = parent;
+ }
+
+ // ---- Actual implementation of Extract Style modification computation ----
+
+ /**
+ * Returns two items: a map from attribute name to a list of attribute nodes of that
+ * name, and a subset of these attributes that fall within the text selection
+ * (used to drive initial selection in the wizard)
+ */
+ Pair<Map<String, List<Attr>>, Set<Attr>> getAvailableAttributes() {
+ mAvailableAttributes = new TreeMap<String, List<Attr>>();
+ Set<Attr> withinSelection = new HashSet<Attr>();
+ for (Element element : getElements()) {
+ IndexedRegion elementRegion = getRegion(element);
+ boolean allIncluded =
+ (mOriginalSelectionStart <= elementRegion.getStartOffset() &&
+ mOriginalSelectionEnd >= elementRegion.getEndOffset());
+
+ NamedNodeMap attributeMap = element.getAttributes();
+ for (int i = 0, n = attributeMap.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributeMap.item(i);
+
+ String name = attribute.getLocalName();
+ if (!isStylableAttribute(name)) {
+ // Don't offer to extract attributes that don't make sense in
+ // styles (like "id" or "style"), or attributes that the user
+ // probably does not want to define in styles (like layout
+ // attributes such as layout_width, or the label of a button etc).
+ // This makes the options offered listed in the wizard simpler.
+ // In special cases where the user *does* want to set one of these
+ // attributes, they can always do it manually so optimize for
+ // the common case here.
+ continue;
+ }
+
+ // Skip attributes that are in a namespace other than the Android one
+ String namespace = attribute.getNamespaceURI();
+ if (namespace != null && !ANDROID_URI.equals(namespace)) {
+ continue;
+ }
+
+ if (!allIncluded) {
+ IndexedRegion region = getRegion(attribute);
+ boolean attributeIncluded = mOriginalSelectionStart < region.getEndOffset() &&
+ mOriginalSelectionEnd >= region.getStartOffset();
+ if (attributeIncluded) {
+ withinSelection.add(attribute);
+ }
+ } else {
+ withinSelection.add(attribute);
+ }
+
+ List<Attr> list = mAvailableAttributes.get(name);
+ if (list == null) {
+ list = new ArrayList<Attr>();
+ mAvailableAttributes.put(name, list);
+ }
+ list.add(attribute);
+ }
+ }
+
+ return Pair.of(mAvailableAttributes, withinSelection);
+ }
+
+ /**
+ * Returns whether the given local attribute name is one the style wizard
+ * should present as a selectable attribute to be extracted.
+ *
+ * @param name the attribute name, not including a namespace prefix
+ * @return true if the name is one that the user can extract
+ */
+ public static boolean isStylableAttribute(String name) {
+ return !(name == null
+ || name.equals(ATTR_ID)
+ || name.startsWith(ATTR_STYLE)
+ || (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) &&
+ !name.startsWith(ATTR_LAYOUT_MARGIN))
+ || name.equals(ATTR_TEXT)
+ || name.equals(ATTR_HINT)
+ || name.equals(ATTR_SRC)
+ || name.equals(ATTR_ON_CLICK));
+ }
+
+ IFile getStyleFile(IProject project) {
+ return project.getFile(new Path(FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP
+ + mStyleFileName));
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ List<Change> changes = new ArrayList<Change>();
+ if (mChosenAttributes.size() == 0) {
+ return changes;
+ }
+
+ IFile file = getStyleFile(mDelegate.getEditor().getProject());
+ boolean createFile = !file.exists();
+ int insertAtIndex;
+ String initialIndent = null;
+ if (!createFile) {
+ Pair<Integer, String> context = computeInsertContext(file);
+ insertAtIndex = context.getFirst();
+ initialIndent = context.getSecond();
+ } else {
+ insertAtIndex = 0;
+ }
+
+ TextFileChange addFile = new TextFileChange("Create new separate style declaration", file);
+ addFile.setTextType(EXT_XML);
+ changes.add(addFile);
+ String styleString = computeStyleDeclaration(createFile, initialIndent);
+ addFile.setEdit(new InsertEdit(insertAtIndex, styleString));
+
+ // Remove extracted attributes?
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ if (mRemoveExtracted || mRemoveAll) {
+ for (Attr attribute : mChosenAttributes) {
+ List<Attr> list = mAvailableAttributes.get(attribute.getLocalName());
+ for (Attr attr : list) {
+ if (mRemoveAll || attr.getValue().equals(attribute.getValue())) {
+ removeAttribute(rootEdit, attr);
+ }
+ }
+ }
+ }
+
+ // Set the style attribute?
+ if (mApplyStyle) {
+ for (Element element : getElements()) {
+ String value = PREFIX_RESOURCE_REF + REFERENCE_STYLE + mStyleName;
+ setAttribute(rootEdit, element, null, null, ATTR_STYLE, value);
+ }
+ }
+
+ if (rootEdit.hasChildren()) {
+ IFile sourceFile = mDelegate.getEditor().getInputFile();
+ if (sourceFile == null) {
+ return changes;
+ }
+ TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
+ change.setTextType(EXT_XML);
+ changes.add(change);
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+ }
+
+ change.setEdit(rootEdit);
+ }
+
+ return changes;
+ }
+
+ private String computeStyleDeclaration(boolean createFile, String initialIndent) {
+ StringBuilder sb = new StringBuilder();
+ if (createFile) {
+ sb.append(NewXmlFileWizard.XML_HEADER_LINE);
+ sb.append('<').append(TAG_RESOURCES).append(' ');
+ sb.append(XMLNS_PREFIX).append(ANDROID_NS_NAME).append('=').append('"');
+ sb.append(ANDROID_URI);
+ sb.append('"').append('>').append('\n');
+ }
+
+ // Indent. Use the existing indent found for previous <style> elements in
+ // the resource file - but if that indent was 0 (e.g. <style> elements are
+ // at the left margin) only use it to indent the style elements and use a real
+ // nonzero indent for its children.
+ String indent = " "; //$NON-NLS-1$
+ if (initialIndent == null) {
+ initialIndent = indent;
+ } else if (initialIndent.length() > 0) {
+ indent = initialIndent;
+ }
+ sb.append(initialIndent);
+ String styleTag = "style"; //$NON-NLS-1$ // TODO - use constant in parallel changeset
+ sb.append('<').append(styleTag).append(' ').append(ATTR_NAME).append('=').append('"');
+ sb.append(mStyleName);
+ sb.append('"');
+ if (mParent != null) {
+ sb.append(' ').append(ATTR_PARENT).append('=').append('"');
+ sb.append(mParent);
+ sb.append('"');
+ }
+ sb.append('>').append('\n');
+
+ for (Attr attribute : mChosenAttributes) {
+ sb.append(initialIndent).append(indent);
+ sb.append('<').append(TAG_ITEM).append(' ').append(ATTR_NAME).append('=').append('"');
+ // We've already enforced that regardless of prefix, only attributes with
+ // an Android namespace can be in the set of chosen attributes. Rewrite the
+ // prefix to android here.
+ if (attribute.getPrefix() != null) {
+ sb.append(ANDROID_NS_NAME_PREFIX);
+ }
+ sb.append(attribute.getLocalName());
+ sb.append('"').append('>');
+ sb.append(attribute.getValue());
+ sb.append('<').append('/').append(TAG_ITEM).append('>').append('\n');
+ }
+ sb.append(initialIndent).append('<').append('/').append(styleTag).append('>').append('\n');
+
+ if (createFile) {
+ sb.append('<').append('/').append(TAG_RESOURCES).append('>').append('\n');
+ }
+ String styleString = sb.toString();
+ return styleString;
+ }
+
+ /** Computes the location in the file to insert the new style element at, as well as
+ * the exact indent string to use to indent the {@code <style>} element.
+ * @param file the styles.xml file to insert into
+ * @return a pair of an insert offset and an indent string
+ */
+ private Pair<Integer, String> computeInsertContext(final IFile file) {
+ int insertAtIndex = -1;
+ // Find the insert of the final </resources> item where we will insert
+ // the new style elements.
+ String indent = null;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ try {
+ model = modelManager.getModelForRead(file);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ IDOMDocument otherDocument = domModel.getDocument();
+ Element root = otherDocument.getDocumentElement();
+ Node lastChild = root.getLastChild();
+ if (lastChild != null) {
+ if (lastChild instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) lastChild;
+ insertAtIndex = region.getStartOffset() + region.getLength();
+ }
+
+ // Compute indent
+ while (lastChild != null) {
+ if (lastChild.getNodeType() == Node.ELEMENT_NODE) {
+ IStructuredDocument document = model.getStructuredDocument();
+ indent = AndroidXmlEditor.getIndent(document, lastChild);
+ break;
+ }
+ lastChild = lastChild.getPreviousSibling();
+ }
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ if (insertAtIndex == -1) {
+ String contents = AdtPlugin.readFile(file);
+ insertAtIndex = contents.indexOf("</" + TAG_RESOURCES + ">"); //$NON-NLS-1$
+ if (insertAtIndex == -1) {
+ insertAtIndex = contents.length();
+ }
+ }
+
+ return Pair.of(insertAtIndex, indent);
+ }
+
+ @Override
+ VisualRefactoringWizard createWizard() {
+ return new ExtractStyleWizard(this, mDelegate);
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.extract.style", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ExtractStyleRefactoring(args);
+ }
+ }
+
+ /**
+ * Determines the parent style to be used for this refactoring
+ *
+ * @return the parent style to be used for this refactoring
+ */
+ public String getParentStyle() {
+ Set<String> styles = new HashSet<String>();
+ for (Element element : getElements()) {
+ // Includes "" for elements not setting the style
+ styles.add(element.getAttribute(ATTR_STYLE));
+ }
+
+ if (styles.size() > 1) {
+ // The elements differ in what style attributes they are set to
+ return null;
+ }
+
+ String style = styles.iterator().next();
+ if (style != null && style.length() > 0) {
+ return style;
+ }
+
+ // None of the elements set the style -- see if they have the same widget types
+ // and if so offer to extend the theme style for that widget type
+
+ Set<String> types = new HashSet<String>();
+ for (Element element : getElements()) {
+ types.add(element.getTagName());
+ }
+
+ if (types.size() == 1) {
+ String view = DescriptorsUtils.getBasename(types.iterator().next());
+
+ ResourceResolver resolver = mDelegate.getGraphicalEditor().getResourceResolver();
+ // Look up the theme item name, which for a Button would be "buttonStyle", and so on.
+ String n = Character.toLowerCase(view.charAt(0)) + view.substring(1)
+ + "Style"; //$NON-NLS-1$
+ ResourceValue value = resolver.findItemInTheme(n);
+ if (value != null) {
+ ResourceValue resolvedValue = resolver.resolveResValue(value);
+ String name = resolvedValue.getName();
+ if (name != null) {
+ if (resolvedValue.isFramework()) {
+ return PREFIX_ANDROID + name;
+ } else {
+ return name;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleWizard.java
new file mode 100644
index 000000000..187452d21
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleWizard.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static org.eclipse.jface.viewers.StyledString.DECORATIONS_STYLER;
+import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.StyledCellLabelProvider;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+import org.w3c.dom.Attr;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class ExtractStyleWizard extends VisualRefactoringWizard {
+ public ExtractStyleWizard(ExtractStyleRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle(ref.getName());
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ String initialName = "styleName";
+ addPage(new InputPage(mDelegate.getEditor().getProject(), initialName));
+ }
+
+ /**
+ * Wizard page which inputs parameters for the {@link ExtractStyleRefactoring}
+ * operation
+ */
+ private static class InputPage extends VisualRefactoringInputPage {
+ private final IProject mProject;
+ private final String mSuggestedName;
+ private Text mNameText;
+ private Table mTable;
+ private Button mRemoveExtracted;
+ private Button mSetStyle;
+ private Button mRemoveAll;
+ private Button mExtend;
+ private CheckboxTableViewer mCheckedView;
+
+ private String mParentStyle;
+ private Set<Attr> mInSelection;
+ private List<Attr> mAllAttributes;
+ private int mElementCount;
+ private Map<Attr, Integer> mFrequencyCount;
+ private Set<Attr> mShown;
+ private List<Attr> mInitialChecked;
+ private List<Attr> mAllChecked;
+ private List<Map.Entry<String, List<Attr>>> mRoot;
+ private Map<String, List<Attr>> mAvailableAttributes;
+
+ public InputPage(IProject project, String suggestedName) {
+ super("ExtractStyleInputPage");
+ mProject = project;
+ mSuggestedName = suggestedName;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ initialize();
+
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label nameLabel = new Label(composite, SWT.NONE);
+ nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ nameLabel.setText("Style Name:");
+
+ mNameText = new Text(composite, SWT.BORDER);
+ mNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mNameText.addModifyListener(mModifyValidateListener);
+
+ mRemoveExtracted = new Button(composite, SWT.CHECK);
+ mRemoveExtracted.setSelection(true);
+ mRemoveExtracted.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
+ mRemoveExtracted.setText("Remove extracted attributes");
+ mRemoveExtracted.addSelectionListener(mSelectionValidateListener);
+
+ mRemoveAll = new Button(composite, SWT.CHECK);
+ mRemoveAll.setSelection(false);
+ mRemoveAll.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
+ mRemoveAll.setText("Remove all extracted attributes regardless of value");
+ mRemoveAll.addSelectionListener(mSelectionValidateListener);
+
+ boolean defaultSetStyle = false;
+ if (mParentStyle != null) {
+ mExtend = new Button(composite, SWT.CHECK);
+ mExtend.setSelection(true);
+ mExtend.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
+ mExtend.setText(String.format("Extend %1$s", mParentStyle));
+ mExtend.addSelectionListener(mSelectionValidateListener);
+ defaultSetStyle = true;
+ }
+
+ mSetStyle = new Button(composite, SWT.CHECK);
+ mSetStyle.setSelection(defaultSetStyle);
+ mSetStyle.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
+ mSetStyle.setText("Set style attribute on extracted elements");
+ mSetStyle.addSelectionListener(mSelectionValidateListener);
+
+ new Label(composite, SWT.NONE);
+ new Label(composite, SWT.NONE);
+
+ Label tableLabel = new Label(composite, SWT.NONE);
+ tableLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ tableLabel.setText("Choose style attributes to extract:");
+
+ mCheckedView = CheckboxTableViewer.newCheckList(composite, SWT.BORDER
+ | SWT.FULL_SELECTION | SWT.HIDE_SELECTION);
+ mTable = mCheckedView.getTable();
+ mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 2));
+ ((GridData) mTable.getLayoutData()).heightHint = 200;
+
+ mCheckedView.setContentProvider(new ArgumentContentProvider());
+ mCheckedView.setLabelProvider(new ArgumentLabelProvider());
+ mCheckedView.setInput(mRoot);
+ final Object[] initialSelection = mInitialChecked.toArray();
+ mCheckedView.setCheckedElements(initialSelection);
+
+ mCheckedView.addCheckStateListener(new ICheckStateListener() {
+ @Override
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ // Try to disable other elements that conflict with this
+ boolean isChecked = event.getChecked();
+ if (isChecked) {
+ Attr attribute = (Attr) event.getElement();
+ List<Attr> list = mAvailableAttributes.get(attribute.getLocalName());
+ for (Attr other : list) {
+ if (other != attribute && mShown.contains(other)) {
+ mCheckedView.setChecked(other, false);
+ }
+ }
+ }
+
+ validatePage();
+ }
+ });
+
+ // Select All / Deselect All
+ Composite buttonForm = new Composite(composite, SWT.NONE);
+ buttonForm.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
+ rowLayout.marginTop = 0;
+ rowLayout.marginLeft = 0;
+ buttonForm.setLayout(rowLayout);
+ Button checkAllButton = new Button(buttonForm, SWT.FLAT);
+ checkAllButton.setText("Select All");
+ checkAllButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // Select "all" (but not conflicting settings)
+ mCheckedView.setCheckedElements(mAllChecked.toArray());
+ validatePage();
+ }
+ });
+ Button uncheckAllButton = new Button(buttonForm, SWT.FLAT);
+ uncheckAllButton.setText("Deselect All");
+ uncheckAllButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mCheckedView.setAllChecked(false);
+ validatePage();
+ }
+ });
+
+ // Initialize UI:
+ if (mSuggestedName != null) {
+ mNameText.setText(mSuggestedName);
+ }
+
+ setControl(composite);
+ validatePage();
+ }
+
+ private void initialize() {
+ ExtractStyleRefactoring ref = (ExtractStyleRefactoring) getRefactoring();
+
+ mElementCount = ref.getElements().size();
+
+ mParentStyle = ref.getParentStyle();
+
+ // Set up data structures needed by the wizard -- to compute the actual
+ // attributes to list in the wizard (there could be multiple attributes
+ // of the same name (on different elements) and we only want to show one, etc.)
+
+ Pair<Map<String, List<Attr>>, Set<Attr>> result = ref.getAvailableAttributes();
+ // List of all available attributes on the selected elements
+ mAvailableAttributes = result.getFirst();
+ // Set of attributes that overlap the text selection, or all attributes if
+ // wizard is invoked from GUI context
+ mInSelection = result.getSecond();
+
+ // The root data structure, which we set as the table root. The content provider
+ // will produce children from it. This is the entry set of a map from
+ // attribute name to list of attribute nodes for that attribute name.
+ mRoot = new ArrayList<Map.Entry<String, List<Attr>>>(
+ mAvailableAttributes.entrySet());
+
+ // Sort the items by attribute name -- the attribute name is the key
+ // in the entry set above.
+ Collections.sort(mRoot, new Comparator<Map.Entry<String, List<Attr>>>() {
+ @Override
+ public int compare(Map.Entry<String, List<Attr>> e1,
+ Map.Entry<String, List<Attr>> e2) {
+ return e1.getKey().compareTo(e2.getKey());
+ }
+ });
+
+ // Set of attributes actually included in the list shown to the user.
+ // (There could be many additional "aliasing" nodes on other elements
+ // with the same name.) Note however that we DO show multiple attribute
+ // occurrences of the same attribute name: precisely one for each unique -value-
+ // of that attribute.
+ mShown = new HashSet<Attr>();
+
+ // The list of initially checked attributes.
+ mInitialChecked = new ArrayList<Attr>();
+
+ // The list of attributes to be checked if "Select All" is chosen (this is not
+ // the same as *all* attributes, since we need to exclude any conflicts)
+ mAllChecked = new ArrayList<Attr>();
+
+ // All attributes.
+ mAllAttributes = new ArrayList<Attr>();
+
+ // Frequency count, from attribute to integer. Attributes that do not
+ // appear in the list have frequency 1, not 0.
+ mFrequencyCount = new HashMap<Attr, Integer>();
+
+ for (Map.Entry<String, List<Attr>> entry : mRoot) {
+ // Iterate over all attributes of the same name, and sort them
+ // by value. This will make it easy to list each -unique- value in the
+ // wizard.
+ List<Attr> attrList = entry.getValue();
+ Collections.sort(attrList, new Comparator<Attr>() {
+ @Override
+ public int compare(Attr a1, Attr a2) {
+ return a1.getValue().compareTo(a2.getValue());
+ }
+ });
+
+ // We need to compute a couple of things: the frequency for all identical
+ // values (and stash them in the frequency map), and record the first
+ // attribute with a particular value into the list of attributes to
+ // be shown.
+ Attr prevAttr = null;
+ String prev = null;
+ List<Attr> uniqueValueAttrs = new ArrayList<Attr>();
+ for (Attr attr : attrList) {
+ String value = attr.getValue();
+ if (value.equals(prev)) {
+ Integer count = mFrequencyCount.get(prevAttr);
+ if (count == null) {
+ count = Integer.valueOf(2);
+ } else {
+ count = Integer.valueOf(count.intValue() + 1);
+ }
+ mFrequencyCount.put(prevAttr, count);
+ } else {
+ uniqueValueAttrs.add(attr);
+ prev = value;
+ prevAttr = attr;
+ }
+ }
+
+ // Sort the values by frequency (and for equal frequencies, alphabetically
+ // by value)
+ Collections.sort(uniqueValueAttrs, new Comparator<Attr>() {
+ @Override
+ public int compare(Attr a1, Attr a2) {
+ Integer f1 = mFrequencyCount.get(a1);
+ Integer f2 = mFrequencyCount.get(a2);
+ if (f1 == null) {
+ f1 = Integer.valueOf(1);
+ }
+ if (f2 == null) {
+ f2 = Integer.valueOf(1);
+ }
+ int delta = f2.intValue() - f1.intValue();
+ if (delta != 0) {
+ return delta;
+ } else {
+ return a1.getValue().compareTo(a2.getValue());
+ }
+ }
+ });
+
+ // Add the items in order, and select those attributes that overlap
+ // the selection
+ mAllAttributes.addAll(uniqueValueAttrs);
+ mShown.addAll(uniqueValueAttrs);
+ Attr first = uniqueValueAttrs.get(0);
+ mAllChecked.add(first);
+ if (mInSelection.contains(first)) {
+ mInitialChecked.add(first);
+ }
+ }
+ }
+
+ @Override
+ protected boolean validatePage() {
+ boolean ok = true;
+
+ String text = mNameText.getText().trim();
+
+ if (text.length() == 0) {
+ setErrorMessage("Provide a name for the new style");
+ ok = false;
+ } else {
+ ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
+ ResourceType.STYLE);
+ String message = validator.isValid(text);
+ if (message != null) {
+ setErrorMessage(message);
+ ok = false;
+ }
+ }
+
+ Object[] checkedElements = mCheckedView.getCheckedElements();
+ if (checkedElements.length == 0) {
+ setErrorMessage("Choose at least one attribute to extract");
+ ok = false;
+ }
+
+ if (ok) {
+ setErrorMessage(null);
+
+ // Record state
+ ExtractStyleRefactoring refactoring = (ExtractStyleRefactoring) getRefactoring();
+ refactoring.setStyleName(text);
+ refactoring.setRemoveExtracted(mRemoveExtracted.getSelection());
+ refactoring.setRemoveAll(mRemoveAll.getSelection());
+ refactoring.setApplyStyle(mSetStyle.getSelection());
+ if (mExtend != null && mExtend.getSelection()) {
+ refactoring.setParent(mParentStyle);
+ }
+ List<Attr> attributes = new ArrayList<Attr>();
+ for (Object o : checkedElements) {
+ attributes.add((Attr) o);
+ }
+ refactoring.setChosenAttributes(attributes);
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+
+ private class ArgumentLabelProvider extends StyledCellLabelProvider {
+ public ArgumentLabelProvider() {
+ }
+
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ Attr attribute = (Attr) element;
+
+ StyledString styledString = new StyledString();
+ styledString.append(attribute.getLocalName());
+ styledString.append(" = ", QUALIFIER_STYLER);
+ styledString.append(attribute.getValue());
+
+ if (mElementCount > 1) {
+ Integer f = mFrequencyCount.get(attribute);
+ String s = String.format(" (in %d/%d elements)",
+ f != null ? f.intValue(): 1, mElementCount);
+ styledString.append(s, DECORATIONS_STYLER);
+ }
+ cell.setText(styledString.toString());
+ cell.setStyleRanges(styledString.getStyleRanges());
+ super.update(cell);
+ }
+ }
+
+ private class ArgumentContentProvider implements IStructuredContentProvider {
+ public ArgumentContentProvider() {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement == mRoot) {
+ return mAllAttributes.toArray();
+ }
+
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java
new file mode 100644
index 000000000..fe673a5b7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java
@@ -0,0 +1,988 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_BACKGROUND;
+import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.RADIO_GROUP;
+import static com.android.SdkConstants.RELATIVE_LAYOUT;
+import static com.android.SdkConstants.SPACE;
+import static com.android.SdkConstants.TABLE_LAYOUT;
+import static com.android.SdkConstants.TABLE_ROW;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_HORIZONTAL;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_HORIZ_MASK;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK;
+
+import com.android.ide.common.api.IViewMetadata.FillPreference;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.GravityHelper;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MalformedTreeException;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class which performs the bulk of the layout conversion to grid layout
+ * <p>
+ * Future enhancements:
+ * <ul>
+ * <li>Render the layout at multiple screen sizes and analyze how the widget bounds
+ * change and use this to infer gravity
+ * <li> Use the layout_width and layout_height attributes on views to infer column and
+ * row flexibility (and as mentioned above, possibly layout_weight).
+ * move and stretch and use that to add in additional constraints
+ * <li> Take into account existing margins and add/subtract those from the
+ * bounds computations and either clear or update them.
+ * <li>Try to reorder elements into their natural order
+ * <li> Try to preserve spacing? Right now everything gets converted into a compact
+ * grid with no spacing between the views; consider inserting {@code <Space>} views
+ * with dimensions based on existing distances.
+ * </ul>
+ */
+@SuppressWarnings("restriction") // DOM model access
+class GridLayoutConverter {
+ private final MultiTextEdit mRootEdit;
+ private final boolean mFlatten;
+ private final Element mLayout;
+ private final ChangeLayoutRefactoring mRefactoring;
+ private final CanvasViewInfo mRootView;
+
+ private List<View> mViews;
+ private String mNamespace;
+ private int mColumnCount;
+
+ /** Creates a new {@link GridLayoutConverter} */
+ GridLayoutConverter(ChangeLayoutRefactoring refactoring,
+ Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
+ mRefactoring = refactoring;
+ mLayout = layout;
+ mFlatten = flatten;
+ mRootEdit = rootEdit;
+ mRootView = rootView;
+ }
+
+ /** Performs conversion from any layout to a RelativeLayout */
+ public void convertToGridLayout() {
+ if (mRootView == null) {
+ return;
+ }
+
+ // Locate the view for the layout
+ CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
+ if (layoutView == null || layoutView.getChildren().size() == 0) {
+ // No children. THAT was an easy conversion!
+ return;
+ }
+
+ // Study the layout and get information about how to place individual elements
+ GridModel gridModel = new GridModel(layoutView, mLayout, mFlatten);
+ mViews = gridModel.getViews();
+ mColumnCount = gridModel.computeColumnCount();
+
+ deleteRemovedElements(gridModel.getDeletedElements());
+ mNamespace = mRefactoring.getAndroidNamespacePrefix();
+
+ processGravities();
+
+ // Insert space views if necessary
+ insertStretchableSpans();
+
+ // Create/update relative layout constraints
+ assignGridAttributes();
+
+ removeUndefinedAttrs();
+
+ if (mColumnCount > 0) {
+ mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI,
+ mNamespace, ATTR_COLUMN_COUNT, Integer.toString(mColumnCount));
+ }
+ }
+
+ private void insertStretchableSpans() {
+ // Look at the rows and columns and determine if we need to have a stretchable
+ // row and/or a stretchable column in the layout.
+ // In a GridLayout, a row or column is stretchable if it defines a gravity (regardless
+ // of what the gravity is -- in other words, a column is not just stretchable if it
+ // has gravity=fill but also if it has gravity=left). Furthermore, ALL the elements
+ // in the row/column have to be stretchable for the overall row/column to be
+ // considered stretchable.
+
+ // Map from row index to boolean for "is the row fixed/inflexible?"
+ Map<Integer, Boolean> rowFixed = new HashMap<Integer, Boolean>();
+ Map<Integer, Boolean> columnFixed = new HashMap<Integer, Boolean>();
+ for (View view : mViews) {
+ if (view.mElement == mLayout) {
+ continue;
+ }
+
+ int gravity = GravityHelper.getGravity(view.mGravity, 0);
+ if ((gravity & GRAVITY_HORIZ_MASK) == 0) {
+ columnFixed.put(view.mCol, true);
+ } else if (!columnFixed.containsKey(view.mCol)) {
+ columnFixed.put(view.mCol, false);
+ }
+ if ((gravity & GRAVITY_VERT_MASK) == 0) {
+ rowFixed.put(view.mRow, true);
+ } else if (!rowFixed.containsKey(view.mRow)) {
+ rowFixed.put(view.mRow, false);
+ }
+ }
+
+ boolean hasStretchableRow = false;
+ boolean hasStretchableColumn = false;
+ for (boolean fixed : rowFixed.values()) {
+ if (!fixed) {
+ hasStretchableRow = true;
+ }
+ }
+ for (boolean fixed : columnFixed.values()) {
+ if (!fixed) {
+ hasStretchableColumn = true;
+ }
+ }
+
+ if (!hasStretchableRow || !hasStretchableColumn) {
+ // Insert <Space> to hold stretchable space
+ // TODO: May also have to increment column count!
+ int offset = 0; // WHERE?
+
+ String gridLayout = mLayout.getTagName();
+ if (mLayout instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) mLayout;
+ int end = region.getEndOffset();
+ // TODO: Look backwards for the "</"
+ // (and can it ever be <foo/>) ?
+ end -= (gridLayout.length() + 3); // 3: <, /, >
+ offset = end;
+ }
+
+ int row = rowFixed.size();
+ int column = columnFixed.size();
+ StringBuilder sb = new StringBuilder(64);
+ String spaceTag = SPACE;
+ IFile file = mRefactoring.getFile();
+ if (file != null) {
+ spaceTag = SupportLibraryHelper.getTagFor(file.getProject(), FQCN_SPACE);
+ if (spaceTag.equals(FQCN_SPACE)) {
+ spaceTag = SPACE;
+ }
+ }
+
+ sb.append('<').append(spaceTag).append(' ');
+ String gravity;
+ if (!hasStretchableRow && !hasStretchableColumn) {
+ gravity = GRAVITY_VALUE_FILL;
+ } else if (!hasStretchableRow) {
+ gravity = GRAVITY_VALUE_FILL_VERTICAL;
+ } else {
+ assert !hasStretchableColumn;
+ gravity = GRAVITY_VALUE_FILL_HORIZONTAL;
+ }
+
+ sb.append(mNamespace).append(':');
+ sb.append(ATTR_LAYOUT_GRAVITY).append('=').append('"').append(gravity);
+ sb.append('"').append(' ');
+
+ sb.append(mNamespace).append(':');
+ sb.append(ATTR_LAYOUT_ROW).append('=').append('"').append(Integer.toString(row));
+ sb.append('"').append(' ');
+
+ sb.append(mNamespace).append(':');
+ sb.append(ATTR_LAYOUT_COLUMN).append('=').append('"').append(Integer.toString(column));
+ sb.append('"').append('/').append('>');
+
+ String space = sb.toString();
+ InsertEdit replace = new InsertEdit(offset, space);
+ mRootEdit.addChild(replace);
+
+ mColumnCount++;
+ }
+ }
+
+ private void removeUndefinedAttrs() {
+ ViewElementDescriptor descriptor = mRefactoring.getElementDescriptor(FQCN_GRID_LAYOUT);
+ if (descriptor == null) {
+ return;
+ }
+
+ Set<String> defined = new HashSet<String>();
+ AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes();
+ for (AttributeDescriptor attribute : layoutAttributes) {
+ defined.add(attribute.getXmlLocalName());
+ }
+
+ for (View view : mViews) {
+ Element child = view.mElement;
+
+ List<Attr> attributes = mRefactoring.findLayoutAttributes(child);
+ for (Attr attribute : attributes) {
+ String name = attribute.getLocalName();
+ if (!defined.contains(name)) {
+ // Remove it
+ try {
+ mRefactoring.removeAttribute(mRootEdit, child, attribute.getNamespaceURI(),
+ name);
+ } catch (MalformedTreeException mte) {
+ // Sometimes refactoring has modified attribute; not
+ // removing
+ // it is non-fatal so just warn instead of letting
+ // refactoring
+ // operation abort
+ AdtPlugin.log(IStatus.WARNING,
+ "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$
+ "already modified during refactoring?", //$NON-NLS-1$
+ attribute.getLocalName());
+ }
+ }
+ }
+ }
+ }
+
+ /** Removes any elements targeted for deletion */
+ private void deleteRemovedElements(List<Element> delete) {
+ if (mFlatten && delete.size() > 0) {
+ for (Element element : delete) {
+ mRefactoring.removeElementTags(mRootEdit, element, delete,
+ false /*changeIndentation*/);
+ }
+ }
+ }
+
+ /**
+ * Creates refactoring edits which adds or updates the grid attributes
+ */
+ private void assignGridAttributes() {
+ // We always convert to horizontal grid layouts for now
+ mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI,
+ mNamespace, ATTR_ORIENTATION, VALUE_HORIZONTAL);
+
+ assignCellAttributes();
+ }
+
+ /**
+ * Assign cell attributes to the table, skipping those that will be implied
+ * by the grid model
+ */
+ private void assignCellAttributes() {
+ int implicitRow = 0;
+ int implicitColumn = 0;
+ int nextRow = 0;
+ for (View view : mViews) {
+ Element element = view.getElement();
+ if (element == mLayout) {
+ continue;
+ }
+
+ int row = view.getRow();
+ int column = view.getColumn();
+
+ if (column != implicitColumn && (implicitColumn > 0 || implicitRow > 0)) {
+ mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
+ mNamespace, ATTR_LAYOUT_COLUMN, Integer.toString(column));
+ if (column < implicitColumn) {
+ implicitRow++;
+ }
+ implicitColumn = column;
+ }
+ if (row != implicitRow) {
+ mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
+ mNamespace, ATTR_LAYOUT_ROW, Integer.toString(row));
+ implicitRow = row;
+ }
+
+ int rowSpan = view.getRowSpan();
+ int columnSpan = view.getColumnSpan();
+ assert columnSpan >= 1;
+
+ if (rowSpan > 1) {
+ mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
+ mNamespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan));
+ }
+ if (columnSpan > 1) {
+ mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
+ mNamespace, ATTR_LAYOUT_COLUMN_SPAN,
+ Integer.toString(columnSpan));
+ }
+ nextRow = Math.max(nextRow, row + rowSpan);
+
+ // wrap_content is redundant in GridLayouts
+ Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) {
+ mRefactoring.removeAttribute(mRootEdit, width);
+ }
+ Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) {
+ mRefactoring.removeAttribute(mRootEdit, height);
+ }
+
+ // Fix up children moved from LinearLayouts that have "invalid" sizes that
+ // was intended for layout weight handling in their old parent
+ if (LINEAR_LAYOUT.equals(element.getParentNode().getNodeName())) {
+ convert0dipToWrapContent(element);
+ }
+
+ implicitColumn += columnSpan;
+ if (implicitColumn >= mColumnCount) {
+ implicitColumn = 0;
+ assert nextRow > implicitRow;
+ implicitRow = nextRow;
+ }
+ }
+ }
+
+ private void processGravities() {
+ for (View view : mViews) {
+ Element element = view.getElement();
+ if (element == mLayout) {
+ continue;
+ }
+
+ Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ String gravity = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
+ String newGravity = null;
+ if (width != null && (VALUE_MATCH_PARENT.equals(width.getValue()) ||
+ VALUE_FILL_PARENT.equals(width.getValue()))) {
+ mRefactoring.removeAttribute(mRootEdit, width);
+ newGravity = gravity = GRAVITY_VALUE_FILL_HORIZONTAL;
+ }
+ if (height != null && (VALUE_MATCH_PARENT.equals(height.getValue()) ||
+ VALUE_FILL_PARENT.equals(height.getValue()))) {
+ mRefactoring.removeAttribute(mRootEdit, height);
+ if (newGravity == GRAVITY_VALUE_FILL_HORIZONTAL) {
+ newGravity = GRAVITY_VALUE_FILL;
+ } else {
+ newGravity = GRAVITY_VALUE_FILL_VERTICAL;
+ }
+ gravity = newGravity;
+ }
+
+ if (gravity == null || gravity.length() == 0) {
+ ElementDescriptor descriptor = view.mInfo.getUiViewNode().getDescriptor();
+ if (descriptor instanceof ViewElementDescriptor) {
+ ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) descriptor;
+ String fqcn = viewDescriptor.getFullClassName();
+ FillPreference fill = ViewMetadataRepository.get().getFillPreference(fqcn);
+ gravity = GridLayoutRule.computeDefaultGravity(fill);
+ if (gravity != null) {
+ newGravity = gravity;
+ }
+ }
+ }
+
+ if (newGravity != null) {
+ mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
+ mNamespace, ATTR_LAYOUT_GRAVITY, newGravity);
+ }
+
+ view.mGravity = newGravity != null ? newGravity : gravity;
+ }
+ }
+
+
+ /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
+ private void convert0dipToWrapContent(Element child) {
+ // Must convert layout_height="0dip" to layout_height="wrap_content".
+ // (And since wrap_content is the default, what we really do is remove
+ // the attribute completely.)
+ // 0dip is a special trick used in linear layouts in the presence of
+ // weights where 0dip ensures that the height of the view is not taken
+ // into account when distributing the weights. However, when converted
+ // to RelativeLayout this will instead cause the view to actually be assigned
+ // 0 height.
+ Attr height = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ // 0dip, 0dp, 0px, etc
+ if (height != null && height.getValue().startsWith("0")) { //$NON-NLS-1$
+ mRefactoring.removeAttribute(mRootEdit, height);
+ }
+ Attr width = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ if (width != null && width.getValue().startsWith("0")) { //$NON-NLS-1$
+ mRefactoring.removeAttribute(mRootEdit, width);
+ }
+ }
+
+ /**
+ * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
+ * {@link Element}
+ *
+ * @param info the root {@link CanvasViewInfo} to search below
+ * @param element the target element
+ * @return the {@link CanvasViewInfo} which corresponds to the given element
+ */
+ private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
+ if (getElement(info) == element) {
+ return info;
+ }
+
+ for (CanvasViewInfo child : info.getChildren()) {
+ CanvasViewInfo result = findViewForElement(child, element);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
+ private static Element getElement(CanvasViewInfo info) {
+ Node node = info.getUiViewNode().getXmlNode();
+ if (node instanceof Element) {
+ return (Element) node;
+ }
+
+ return null;
+ }
+
+
+ /** Holds layout information about an individual view */
+ private static class View {
+ private final Element mElement;
+ private int mRow = -1;
+ private int mCol = -1;
+ private int mRowSpan = -1;
+ private int mColSpan = -1;
+ private int mX1;
+ private int mY1;
+ private int mX2;
+ private int mY2;
+ private CanvasViewInfo mInfo;
+ private String mGravity;
+
+ public View(CanvasViewInfo view, Element element) {
+ mInfo = view;
+ mElement = element;
+
+ Rectangle b = mInfo.getAbsRect();
+ mX1 = b.x;
+ mX2 = b.x + b.width;
+ mY1 = b.y;
+ mY2 = b.y + b.height;
+ }
+
+ /**
+ * Returns the element for this view
+ *
+ * @return the element for the view
+ */
+ public Element getElement() {
+ return mElement;
+ }
+
+ /**
+ * The assigned row for this view
+ *
+ * @return the assigned row
+ */
+ public int getRow() {
+ return mRow;
+ }
+
+ /**
+ * The assigned column for this view
+ *
+ * @return the assigned column
+ */
+ public int getColumn() {
+ return mCol;
+ }
+
+ /**
+ * The assigned row span for this view
+ *
+ * @return the assigned row span
+ */
+ public int getRowSpan() {
+ return mRowSpan;
+ }
+
+ /**
+ * The assigned column span for this view
+ *
+ * @return the assigned column span
+ */
+ public int getColumnSpan() {
+ return mColSpan;
+ }
+
+ /**
+ * The left edge of the view to be used for placement
+ *
+ * @return the left edge x coordinate
+ */
+ public int getLeftEdge() {
+ return mX1;
+ }
+
+ /**
+ * The top edge of the view to be used for placement
+ *
+ * @return the top edge y coordinate
+ */
+ public int getTopEdge() {
+ return mY1;
+ }
+
+ /**
+ * The right edge of the view to be used for placement
+ *
+ * @return the right edge x coordinate
+ */
+ public int getRightEdge() {
+ return mX2;
+ }
+
+ /**
+ * The bottom edge of the view to be used for placement
+ *
+ * @return the bottom edge y coordinate
+ */
+ public int getBottomEdge() {
+ return mY2;
+ }
+
+ @Override
+ public String toString() {
+ return "View(" + VisualRefactoring.getId(mElement) + ": " + mX1 + "," + mY1 + ")";
+ }
+ }
+
+ /** Grid model for the views found in the view hierarchy, partitioned into rows and columns */
+ private static class GridModel {
+ private final List<View> mViews = new ArrayList<View>();
+ private final List<Element> mDelete = new ArrayList<Element>();
+ private final Map<Element, View> mElementToView = new HashMap<Element, View>();
+ private Element mLayout;
+ private boolean mFlatten;
+
+ GridModel(CanvasViewInfo view, Element layout, boolean flatten) {
+ mLayout = layout;
+ mFlatten = flatten;
+
+ scan(view, true);
+ analyzeKnownLayouts();
+ initializeColumns();
+ initializeRows();
+ mDelete.remove(getElement(view));
+ }
+
+ /**
+ * Returns the {@link View} objects to be placed in the grid
+ *
+ * @return list of {@link View} objects, never null but possibly empty
+ */
+ public List<View> getViews() {
+ return mViews;
+ }
+
+ /**
+ * Returns the list of elements that are scheduled for deletion in the
+ * flattening operation
+ *
+ * @return elements to be deleted, never null but possibly empty
+ */
+ public List<Element> getDeletedElements() {
+ return mDelete;
+ }
+
+ /**
+ * Compute and return column count
+ *
+ * @return the column count
+ */
+ public int computeColumnCount() {
+ int columnCount = 0;
+ for (View view : mViews) {
+ if (view.getElement() == mLayout) {
+ continue;
+ }
+
+ int column = view.getColumn();
+ int columnSpan = view.getColumnSpan();
+ if (column + columnSpan > columnCount) {
+ columnCount = column + columnSpan;
+ }
+ }
+ return columnCount;
+ }
+
+ /**
+ * Initializes the column and columnSpan attributes of the views
+ */
+ private void initializeColumns() {
+ // Now initialize table view row, column and spans
+ Map<Integer, List<View>> mColumnViews = new HashMap<Integer, List<View>>();
+ for (View view : mViews) {
+ if (view.mElement == mLayout) {
+ continue;
+ }
+ int x = view.getLeftEdge();
+ List<View> list = mColumnViews.get(x);
+ if (list == null) {
+ list = new ArrayList<View>();
+ mColumnViews.put(x, list);
+ }
+ list.add(view);
+ }
+
+ List<Integer> columnOffsets = new ArrayList<Integer>(mColumnViews.keySet());
+ Collections.sort(columnOffsets);
+
+ int columnIndex = 0;
+ for (Integer column : columnOffsets) {
+ List<View> views = mColumnViews.get(column);
+ if (views != null) {
+ for (View view : views) {
+ view.mCol = columnIndex;
+ }
+ }
+ columnIndex++;
+ }
+ // Initialize column spans
+ for (View view : mViews) {
+ if (view.mElement == mLayout) {
+ continue;
+ }
+ int index = Collections.binarySearch(columnOffsets, view.getRightEdge());
+ int column;
+ if (index == -1) {
+ // Smaller than the first element; just use the first column
+ column = 0;
+ } else if (index < 0) {
+ column = -(index + 2);
+ } else {
+ column = index;
+ }
+
+ if (column < view.mCol) {
+ column = view.mCol;
+ }
+
+ view.mColSpan = column - view.mCol + 1;
+ }
+ }
+
+ /**
+ * Initializes the row and rowSpan attributes of the views
+ */
+ private void initializeRows() {
+ Map<Integer, List<View>> mRowViews = new HashMap<Integer, List<View>>();
+ for (View view : mViews) {
+ if (view.mElement == mLayout) {
+ continue;
+ }
+ int y = view.getTopEdge();
+ List<View> list = mRowViews.get(y);
+ if (list == null) {
+ list = new ArrayList<View>();
+ mRowViews.put(y, list);
+ }
+ list.add(view);
+ }
+
+ List<Integer> rowOffsets = new ArrayList<Integer>(mRowViews.keySet());
+ Collections.sort(rowOffsets);
+
+ int rowIndex = 0;
+ for (Integer row : rowOffsets) {
+ List<View> views = mRowViews.get(row);
+ if (views != null) {
+ for (View view : views) {
+ view.mRow = rowIndex;
+ }
+ }
+ rowIndex++;
+ }
+
+ // Initialize row spans
+ for (View view : mViews) {
+ if (view.mElement == mLayout) {
+ continue;
+ }
+ int index = Collections.binarySearch(rowOffsets, view.getBottomEdge());
+ int row;
+ if (index == -1) {
+ // Smaller than the first element; just use the first row
+ row = 0;
+ } else if (index < 0) {
+ row = -(index + 2);
+ } else {
+ row = index;
+ }
+
+ if (row < view.mRow) {
+ row = view.mRow;
+ }
+
+ view.mRowSpan = row - view.mRow + 1;
+ }
+ }
+
+ /**
+ * Walks over a given view hierarchy and locates views to be placed in
+ * the grid layout (or deleted if we are flattening the hierarchy)
+ *
+ * @param view the view to analyze
+ * @param isRoot whether this view is the root (which cannot be removed)
+ * @return the {@link View} object for the {@link CanvasViewInfo}
+ * hierarchy we just analyzed, or null
+ */
+ private View scan(CanvasViewInfo view, boolean isRoot) {
+ View added = null;
+ if (!mFlatten || !isRemovableLayout(view)) {
+ added = add(view);
+ if (!isRoot) {
+ return added;
+ }
+ } else {
+ mDelete.add(getElement(view));
+ }
+
+ // Build up a table model of the view
+ for (CanvasViewInfo child : view.getChildren()) {
+ Element childElement = getElement(child);
+
+ // See if this view shares the edge with the removed
+ // parent layout, and if so, record that such that we can
+ // later handle attachments to the removed parent edges
+
+ if (mFlatten && isRemovableLayout(child)) {
+ // When flattening, we want to disregard all layouts and instead
+ // add their children!
+ for (CanvasViewInfo childView : child.getChildren()) {
+ scan(childView, false);
+ }
+ mDelete.add(childElement);
+ } else {
+ scan(child, false);
+ }
+ }
+
+ return added;
+ }
+
+ /** Adds the given {@link CanvasViewInfo} into our internal view list */
+ private View add(CanvasViewInfo info) {
+ Element element = getElement(info);
+ View view = new View(info, element);
+ mViews.add(view);
+ mElementToView.put(element, view);
+ return view;
+ }
+
+ private void analyzeKnownLayouts() {
+ Set<Element> parents = new HashSet<Element>();
+ for (View view : mViews) {
+ Node parent = view.getElement().getParentNode();
+ if (parent instanceof Element) {
+ parents.add((Element) parent);
+ }
+ }
+
+ List<Collection<View>> rowGroups = new ArrayList<Collection<View>>();
+ List<Collection<View>> columnGroups = new ArrayList<Collection<View>>();
+ for (Element parent : parents) {
+ String tagName = parent.getTagName();
+ if (tagName.equals(LINEAR_LAYOUT) || tagName.equals(TABLE_LAYOUT) ||
+ tagName.equals(TABLE_ROW) || tagName.equals(RADIO_GROUP)) {
+ Set<View> group = new HashSet<View>();
+ for (Element child : DomUtilities.getChildren(parent)) {
+ View view = mElementToView.get(child);
+ if (view != null) {
+ group.add(view);
+ }
+ }
+ if (group.size() > 1) {
+ boolean isVertical = VALUE_VERTICAL.equals(parent.getAttributeNS(
+ ANDROID_URI, ATTR_ORIENTATION));
+ if (tagName.equals(TABLE_LAYOUT)) {
+ isVertical = true;
+ } else if (tagName.equals(TABLE_ROW)) {
+ isVertical = false;
+ }
+ if (isVertical) {
+ columnGroups.add(group);
+ } else {
+ rowGroups.add(group);
+ }
+ }
+ } else if (tagName.equals(RELATIVE_LAYOUT)) {
+ List<Element> children = DomUtilities.getChildren(parent);
+ for (Element child : children) {
+ View view = mElementToView.get(child);
+ if (view == null) {
+ continue;
+ }
+ NamedNodeMap attributes = child.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attr = (Attr) attributes.item(i);
+ String name = attr.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ boolean alignVertical =
+ name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
+ name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
+ name.equals(ATTR_LAYOUT_ALIGN_BASELINE);
+ boolean alignHorizontal =
+ name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
+ name.equals(ATTR_LAYOUT_ALIGN_RIGHT);
+ if (!alignVertical && !alignHorizontal) {
+ continue;
+ }
+ String value = attr.getValue();
+ if (value.startsWith(ID_PREFIX)
+ || value.startsWith(NEW_ID_PREFIX)) {
+ String targetName = BaseLayoutRule.stripIdPrefix(value);
+ Element target = null;
+ for (Element c : children) {
+ String id = VisualRefactoring.getId(c);
+ if (targetName.equals(BaseLayoutRule.stripIdPrefix(id))) {
+ target = c;
+ break;
+ }
+ }
+ View targetView = mElementToView.get(target);
+ if (targetView != null) {
+ List<View> group = new ArrayList<View>(2);
+ group.add(view);
+ group.add(targetView);
+ if (alignHorizontal) {
+ columnGroups.add(group);
+ } else {
+ assert alignVertical;
+ rowGroups.add(group);
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // TODO: Consider looking for interesting metadata from other layouts
+ }
+ }
+
+ // Assign the same top or left coordinates to the groups to ensure that they
+ // all get positioned in the same row or column
+ for (Collection<View> rowGroup : rowGroups) {
+ // Find the smallest one
+ Iterator<View> iterator = rowGroup.iterator();
+ int smallest = iterator.next().mY1;
+ while (iterator.hasNext()) {
+ smallest = Math.min(smallest, iterator.next().mY1);
+ }
+ for (View view : rowGroup) {
+ view.mY2 -= (view.mY1 - smallest);
+ view.mY1 = smallest;
+ }
+ }
+ for (Collection<View> columnGroup : columnGroups) {
+ Iterator<View> iterator = columnGroup.iterator();
+ int smallest = iterator.next().mX1;
+ while (iterator.hasNext()) {
+ smallest = Math.min(smallest, iterator.next().mX1);
+ }
+ for (View view : columnGroup) {
+ view.mX2 -= (view.mX1 - smallest);
+ view.mX1 = smallest;
+ }
+ }
+ }
+
+ /**
+ * Returns true if the given {@link CanvasViewInfo} represents an element we
+ * should remove in a flattening conversion. We don't want to remove non-layout
+ * views, or layout views that for example contain drawables on their own.
+ */
+ private boolean isRemovableLayout(CanvasViewInfo child) {
+ // The element being converted is NOT removable!
+ Element element = getElement(child);
+ if (element == mLayout) {
+ return false;
+ }
+
+ ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
+ String name = descriptor.getXmlLocalName();
+ if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)
+ || name.equals(TABLE_LAYOUT) || name.equals(TABLE_ROW)) {
+ // Don't delete layouts that provide a background image or gradient
+ if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
+ AdtPlugin.log(IStatus.WARNING,
+ "Did not flatten layout %1$s because it defines a '%2$s' attribute",
+ VisualRefactoring.getId(element), ATTR_BACKGROUND);
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/JavaQuickAssistant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/JavaQuickAssistant.java
new file mode 100644
index 000000000..df5d9eaf3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/JavaQuickAssistant.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringProposal;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.ui.text.java.IInvocationContext;
+import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
+import org.eclipse.jdt.ui.text.java.IProblemLocation;
+
+/**
+ * Quick Assistant for Java files in Android projects
+ */
+public class JavaQuickAssistant implements org.eclipse.jdt.ui.text.java.IQuickAssistProcessor {
+ public JavaQuickAssistant() {
+ }
+
+ @Override
+ public boolean hasAssists(IInvocationContext context) throws CoreException {
+ return true;
+ }
+
+ @Override
+ public IJavaCompletionProposal[] getAssists(IInvocationContext context,
+ IProblemLocation[] locations) throws CoreException {
+ // We should only offer Android quick assists within Android projects.
+ // This can be done by adding this logic to the extension registration:
+ //
+ // <enablement>
+ // <with variable="projectNatures">
+ // <iterate operator="or">
+ // <equals value="com.android.ide.eclipse.adt.AndroidNature"/>
+ // </iterate>
+ // </with>
+ // </enablement>
+ //
+ // However, this causes some errors to be dumped to the log, so instead we filter
+ // out non Android projects programmatically:
+
+ IProject project = context.getCompilationUnit().getJavaProject().getProject();
+ if (project == null || !BaseProjectHelper.isAndroidProject(project)) {
+ return null;
+ }
+
+ ASTNode coveringNode = context.getCoveringNode();
+ if (coveringNode != null && coveringNode.getNodeType() == ASTNode.STRING_LITERAL
+ && coveringNode.getLength() > 2) { // don't extract empty strings (includes quotes)
+ return new IJavaCompletionProposal[] {
+ new ExtractStringProposal(context)
+ };
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java
new file mode 100644
index 000000000..aa8c11999
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.common.resources.ResourceUrl;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceProcessor;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction;
+import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring;
+import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.text.TextSelection;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * QuickAssistProcessor which helps invoke refactoring operations on text elements.
+ */
+@SuppressWarnings("restriction") // XML model
+public class RefactoringAssistant implements IQuickAssistProcessor {
+
+ /**
+ * Creates a new {@link RefactoringAssistant}
+ */
+ public RefactoringAssistant() {
+ }
+
+ @Override
+ public boolean canAssist(IQuickAssistInvocationContext invocationContext) {
+ return true;
+ }
+
+ @Override
+ public boolean canFix(Annotation annotation) {
+ return true;
+ }
+
+ @Override
+ public ICompletionProposal[] computeQuickAssistProposals(
+ IQuickAssistInvocationContext invocationContext) {
+
+ ISourceViewer sourceViewer = invocationContext.getSourceViewer();
+ AndroidXmlEditor xmlEditor = AndroidXmlEditor.fromTextViewer(sourceViewer);
+ if (xmlEditor == null) {
+ return null;
+ }
+
+ IFile file = xmlEditor.getInputFile();
+ if (file == null) {
+ return null;
+ }
+ int offset = invocationContext.getOffset();
+
+ // Ensure that we are over a tag name (for element-based refactoring
+ // operations) or a value (for the extract include refactoring)
+
+ boolean isValue = false;
+ boolean isReferenceValue = false;
+ boolean isTagName = false;
+ boolean isAttributeName = false;
+ boolean isStylableAttribute = false;
+ ResourceUrl resource = null;
+ IStructuredModel model = null;
+ try {
+ model = xmlEditor.getModelForRead();
+ IStructuredDocument doc = model.getStructuredDocument();
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
+ if (subRegion != null) {
+ String type = subRegion.getType();
+ if (type.equals(DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE)) {
+ String value = region.getText(subRegion);
+ // Only extract values that aren't already resources
+ // (and value includes leading ' or ")
+ isValue = true;
+ if (value.startsWith("'@") || value.startsWith("\"@")) { //$NON-NLS-1$ //$NON-NLS-2$
+ isReferenceValue = true;
+ resource = RenameResourceXmlTextAction.findResource(doc, offset);
+ }
+ } else if (type.equals(DOMRegionContext.XML_TAG_NAME)
+ || type.equals(DOMRegionContext.XML_TAG_OPEN)
+ || type.equals(DOMRegionContext.XML_TAG_CLOSE)) {
+ isTagName = true;
+ } else if (type.equals(DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) ) {
+ isAttributeName = true;
+ String name = region.getText(subRegion);
+ int index = name.indexOf(':');
+ if (index != -1) {
+ name = name.substring(index + 1);
+ }
+ isStylableAttribute = ExtractStyleRefactoring.isStylableAttribute(name);
+ } else if (type.equals(DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS)) {
+ // On the edge of an attribute name and an attribute value
+ isAttributeName = true;
+ isStylableAttribute = true;
+ } else if (type.equals(DOMRegionContext.XML_CONTENT)) {
+ resource = RenameResourceXmlTextAction.findResource(doc, offset);
+ }
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
+ if (isTagName || isAttributeName || isValue || resource != null) {
+ StructuredTextEditor structuredEditor = xmlEditor.getStructuredTextEditor();
+ ISelectionProvider provider = structuredEditor.getSelectionProvider();
+ ISelection selection = provider.getSelection();
+ if (selection instanceof ITextSelection) {
+ ITextSelection textSelection = (ITextSelection) selection;
+
+ ITextSelection originalSelection = textSelection;
+
+ // Most of the visual refactorings do not work on text ranges
+ // ...except for Extract Style where the actual attributes overlapping
+ // the selection is going to be the set of eligible attributes
+ boolean selectionOkay = false;
+
+ if (textSelection.getLength() == 0 && !isValue) {
+ selectionOkay = true;
+ ISourceViewer textViewer = xmlEditor.getStructuredSourceViewer();
+ int caretOffset = textViewer.getTextWidget().getCaretOffset();
+ if (caretOffset >= 0) {
+ Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset);
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int startOffset = region.getStartOffset();
+ int length = region.getEndOffset() - region.getStartOffset();
+ textSelection = new TextSelection(startOffset, length);
+ }
+ }
+ }
+
+ if (isValue && !isReferenceValue) {
+ proposals.add(new RefactoringProposal(xmlEditor,
+ new ExtractStringRefactoring(file, xmlEditor, textSelection)));
+ } else if (resource != null) {
+ RenameResourceProcessor processor = new RenameResourceProcessor(
+ file.getProject(), resource.type, resource.name, null);
+ RenameRefactoring refactoring = new RenameRefactoring(processor);
+ proposals.add(new RefactoringProposal(xmlEditor, refactoring));
+ }
+
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(xmlEditor);
+ if (delegate != null) {
+ boolean showStyleFirst = isValue || (isAttributeName && isStylableAttribute);
+ if (showStyleFirst) {
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new ExtractStyleRefactoring(
+ file,
+ delegate,
+ originalSelection,
+ null)));
+ }
+
+ if (selectionOkay) {
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new WrapInRefactoring(
+ file,
+ delegate,
+ textSelection,
+ null)));
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new UnwrapRefactoring(
+ file,
+ delegate,
+ textSelection,
+ null)));
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new ChangeViewRefactoring(
+ file,
+ delegate,
+ textSelection,
+ null)));
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new ChangeLayoutRefactoring(
+ file,
+ delegate,
+ textSelection,
+ null)));
+ }
+
+ // Extract Include must always have an actual block to be extracted
+ if (textSelection.getLength() > 0) {
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new ExtractIncludeRefactoring(
+ file,
+ delegate,
+ textSelection,
+ null)));
+ }
+
+ // If it's not a value or attribute name, don't place it on top
+ if (!showStyleFirst) {
+ proposals.add(new RefactoringProposal(
+ xmlEditor,
+ new ExtractStyleRefactoring(
+ file,
+ delegate,
+ originalSelection,
+ null)));
+ }
+ }
+ }
+ }
+
+ if (proposals.size() == 0) {
+ return null;
+ } else {
+ return proposals.toArray(new ICompletionProposal[proposals.size()]);
+ }
+ }
+
+ @Override
+ public String getErrorMessage() {
+ return null;
+ }
+
+ private static class RefactoringProposal
+ implements ICompletionProposal {
+ private final AndroidXmlEditor mEditor;
+ private final Refactoring mRefactoring;
+
+ RefactoringProposal(AndroidXmlEditor editor, Refactoring refactoring) {
+ super();
+ mEditor = editor;
+ mRefactoring = refactoring;
+ }
+
+ @Override
+ public void apply(IDocument document) {
+ RefactoringWizard wizard = null;
+ if (mRefactoring instanceof VisualRefactoring) {
+ wizard = ((VisualRefactoring) mRefactoring).createWizard();
+ } else if (mRefactoring instanceof ExtractStringRefactoring) {
+ wizard = new ExtractStringWizard((ExtractStringRefactoring) mRefactoring,
+ mEditor.getProject());
+ } else if (mRefactoring instanceof RenameRefactoring) {
+ RenameRefactoring refactoring = (RenameRefactoring) mRefactoring;
+ RenameResourceProcessor processor =
+ (RenameResourceProcessor) refactoring.getProcessor();
+ ResourceType type = processor.getType();
+ wizard = new RenameResourceWizard((RenameRefactoring) mRefactoring, type, false);
+ } else {
+ throw new IllegalArgumentException();
+ }
+
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ op.run(window.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ }
+ }
+
+ @Override
+ public String getAdditionalProposalInfo() {
+ return String.format("Initiates the \"%1$s\" refactoring", mRefactoring.getName());
+ }
+
+ @Override
+ public IContextInformation getContextInformation() {
+ return null;
+ }
+
+ @Override
+ public String getDisplayString() {
+ return mRefactoring.getName();
+ }
+
+ @Override
+ public Image getImage() {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ @Override
+ public Point getSelection(IDocument document) {
+ return null;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java
new file mode 100644
index 000000000..e0d6313bf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java
@@ -0,0 +1,1633 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_BACKGROUND;
+import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.RELATIVE_LAYOUT;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_N_DP;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_HORIZ;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_VERT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_LEFT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_TOP;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK;
+
+import com.android.ide.common.layout.GravityHelper;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.utils.Pair;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class which performs the bulk of the layout conversion to relative layout
+ * <p>
+ * Future enhancements:
+ * <ul>
+ * <li>Render the layout at multiple screen sizes and analyze how the widgets move and
+ * stretch and use that to add in additional constraints
+ * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well
+ * (just need to tweak the "isVertical" interpretation to account for the different defaults,
+ * and perhaps do something about column size properties.
+ * <li> We need to take into account existing margins and clear/update them
+ * </ul>
+ */
+class RelativeLayoutConversionHelper {
+ private final MultiTextEdit mRootEdit;
+ private final boolean mFlatten;
+ private final Element mLayout;
+ private final ChangeLayoutRefactoring mRefactoring;
+ private final CanvasViewInfo mRootView;
+ private List<Element> mDeletedElements;
+
+ RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring,
+ Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
+ mRefactoring = refactoring;
+ mLayout = layout;
+ mFlatten = flatten;
+ mRootEdit = rootEdit;
+ mRootView = rootView;
+ }
+
+ /** Performs conversion from any layout to a RelativeLayout */
+ public void convertToRelative() {
+ if (mRootView == null) {
+ return;
+ }
+
+ // Locate the view for the layout
+ CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
+ if (layoutView == null || layoutView.getChildren().size() == 0) {
+ // No children. THAT was an easy conversion!
+ return;
+ }
+
+ // Study the layout and get information about how to place individual elements
+ List<View> views = analyzeLayout(layoutView);
+
+ // Create/update relative layout constraints
+ createAttachments(views);
+ }
+
+ /** Returns the elements that were deleted, or null */
+ List<Element> getDeletedElements() {
+ return mDeletedElements;
+ }
+
+ /**
+ * Analyzes the given view hierarchy and produces a list of {@link View} objects which
+ * contain placement information for each element
+ */
+ private List<View> analyzeLayout(CanvasViewInfo layoutView) {
+ EdgeList edgeList = new EdgeList(layoutView);
+ mDeletedElements = edgeList.getDeletedElements();
+ deleteRemovedElements(mDeletedElements);
+
+ List<Integer> columnOffsets = edgeList.getColumnOffsets();
+ List<Integer> rowOffsets = edgeList.getRowOffsets();
+
+ // Compute x/y offsets for each row/column index
+ int[] left = new int[columnOffsets.size()];
+ int[] top = new int[rowOffsets.size()];
+
+ Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>();
+ int columnIndex = 0;
+ for (Integer offset : columnOffsets) {
+ left[columnIndex] = offset;
+ xToCol.put(offset, columnIndex++);
+ }
+ Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>();
+ int rowIndex = 0;
+ for (Integer offset : rowOffsets) {
+ top[rowIndex] = offset;
+ yToRow.put(offset, rowIndex++);
+ }
+
+ // Create a complete list of view objects
+ List<View> views = createViews(edgeList, columnOffsets);
+ initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow);
+
+ // Sanity check
+ for (View view : views) {
+ assert view.getLeftEdge() == left[view.mCol];
+ assert view.getTopEdge() == top[view.mRow];
+ assert view.getRightEdge() == left[view.mCol+view.mColSpan];
+ assert view.getBottomEdge() == top[view.mRow+view.mRowSpan];
+ }
+
+ // Ensure that every view has a proper id such that it can be referred to
+ // with a constraint
+ initializeIds(edgeList, views);
+
+ // Attempt to lay the views out in a grid with constraints (though not that widgets
+ // can overlap as well)
+ Grid grid = new Grid(views, left, top);
+ computeKnownConstraints(views, edgeList);
+ computeHorizontalConstraints(grid);
+ computeVerticalConstraints(grid);
+
+ return views;
+ }
+
+ /** Produces a list of {@link View} objects from an {@link EdgeList} */
+ private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) {
+ List<View> views = new ArrayList<View>();
+ for (Integer offset : columnOffsets) {
+ List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
+ if (leftEdgeViews == null) {
+ // must have been a right edge
+ continue;
+ }
+ for (View view : leftEdgeViews) {
+ views.add(view);
+ }
+ }
+ return views;
+ }
+
+ /** Removes any elements targeted for deletion */
+ private void deleteRemovedElements(List<Element> delete) {
+ if (mFlatten && delete.size() > 0) {
+ for (Element element : delete) {
+ mRefactoring.removeElementTags(mRootEdit, element, delete,
+ !AdtPrefs.getPrefs().getFormatGuiXml() /*changeIndentation*/);
+ }
+ }
+ }
+
+ /** Ensures that every element has an id such that it can be referenced from a constraint */
+ private void initializeIds(EdgeList edgeList, List<View> views) {
+ // Ensure that all views have a valid id
+ for (View view : views) {
+ String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null);
+ edgeList.setIdAttributeValue(view, id);
+ }
+ }
+
+ /**
+ * Initializes the column and row indices, as well as any column span and row span
+ * values
+ */
+ private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets,
+ List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) {
+ // Now initialize table view row, column and spans
+ for (Integer offset : columnOffsets) {
+ List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
+ if (leftEdgeViews == null) {
+ // must have been a right edge
+ continue;
+ }
+ for (View view : leftEdgeViews) {
+ Integer col = xToCol.get(view.getLeftEdge());
+ assert col != null;
+ Integer end = xToCol.get(view.getRightEdge());
+ assert end != null;
+
+ view.mCol = col;
+ view.mColSpan = end - col;
+ }
+ }
+
+ for (Integer offset : rowOffsets) {
+ List<View> topEdgeViews = edgeList.getTopEdgeViews(offset);
+ if (topEdgeViews == null) {
+ // must have been a bottom edge
+ continue;
+ }
+ for (View view : topEdgeViews) {
+ Integer row = yToRow.get(view.getTopEdge());
+ assert row != null;
+ Integer end = yToRow.get(view.getBottomEdge());
+ assert end != null;
+
+ view.mRow = row;
+ view.mRowSpan = end - row;
+ }
+ }
+ }
+
+ /**
+ * Creates refactoring edits which adds or updates constraints for the given list of
+ * views
+ */
+ private void createAttachments(List<View> views) {
+ // Make the attachments
+ String namespace = mRefactoring.getAndroidNamespacePrefix();
+ for (View view : views) {
+ for (Pair<String, String> constraint : view.getHorizConstraints()) {
+ mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
+ namespace, constraint.getFirst(), constraint.getSecond());
+ }
+ for (Pair<String, String> constraint : view.getVerticalConstraints()) {
+ mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
+ namespace, constraint.getFirst(), constraint.getSecond());
+ }
+ }
+ }
+
+ /**
+ * Analyzes the existing layouts and layout parameter objects in the document to infer
+ * constraints for layout types that we know about - such as LinearLayout baseline
+ * alignment, weights, gravity, etc.
+ */
+ private void computeKnownConstraints(List<View> views, EdgeList edgeList) {
+ // List of parent layout elements we've already processed. We iterate through all
+ // the -children-, and we ask each for its element parent (which won't have a view)
+ // and we look at the parent's layout attributes and its children layout constraints,
+ // and then we stash away constraints that we can infer. This means that we will
+ // encounter the same parent for every sibling, so that's why there's a map to
+ // prevent duplicate work.
+ Set<Node> seen = new HashSet<Node>();
+
+ for (View view : views) {
+ Element element = view.getElement();
+ Node parent = element.getParentNode();
+ if (seen.contains(parent)) {
+ continue;
+ }
+ seen.add(parent);
+
+ if (parent.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ Element layout = (Element) parent;
+ String layoutName = layout.getTagName();
+
+ if (LINEAR_LAYOUT.equals(layoutName)) {
+ analyzeLinearLayout(edgeList, layout);
+ } else if (RELATIVE_LAYOUT.equals(layoutName)) {
+ analyzeRelativeLayout(edgeList, layout);
+ } else {
+ // Some other layout -- add more conditional handling here
+ // for framelayout, tables, etc.
+ }
+ }
+ }
+
+ /**
+ * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
+ * does not define a weight
+ */
+ private float getWeight(Element linearLayoutChild) {
+ String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
+ if (weight != null && weight.length() > 0) {
+ try {
+ return Float.parseFloat(weight);
+ } catch (NumberFormatException nfe) {
+ AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
+ }
+ }
+
+ return 0.0f;
+ }
+
+ /**
+ * Returns the sum of all the layout weights of the children in the given LinearLayout
+ *
+ * @param linearLayout the layout to compute the total sum for
+ * @return the total sum of all the layout weights in the given layout
+ */
+ private float getWeightSum(Element linearLayout) {
+ float sum = 0;
+ for (Element child : DomUtilities.getChildren(linearLayout)) {
+ sum += getWeight(child);
+ }
+
+ return sum;
+ }
+
+ /**
+ * Analyzes the given LinearLayout and updates the constraints to reflect
+ * relationships it can infer - based on baseline alignment, gravity, order and
+ * weights. This method also removes "0dip" as a special width/height used in
+ * LinearLayouts with weight distribution.
+ */
+ private void analyzeLinearLayout(EdgeList edgeList, Element layout) {
+ boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
+ ATTR_ORIENTATION));
+ View baselineRef = null;
+ if (!isVertical &&
+ !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) {
+ // Baseline alignment. Find the tallest child and set it as the baseline reference.
+ int tallestHeight = 0;
+ View tallest = null;
+ for (Element child : DomUtilities.getChildren(layout)) {
+ View view = edgeList.getView(child);
+ if (view != null && view.getHeight() > tallestHeight) {
+ tallestHeight = view.getHeight();
+ tallest = view;
+ }
+ }
+ if (tallest != null) {
+ baselineRef = tallest;
+ }
+ }
+
+ float weightSum = getWeightSum(layout);
+ float cumulativeWeight = 0;
+
+ List<Element> children = DomUtilities.getChildren(layout);
+ String prevId = null;
+ boolean isFirstChild = true;
+ boolean linkBackwards = true;
+ boolean linkForwards = false;
+
+ for (int index = 0, childCount = children.size(); index < childCount; index++) {
+ Element child = children.get(index);
+
+ View childView = edgeList.getView(child);
+ if (childView == null) {
+ // Could be a nested layout that is being removed etc
+ prevId = null;
+ isFirstChild = false;
+ continue;
+ }
+
+ // Look at the layout_weight attributes and determine whether we should be
+ // attached on the bottom/right or on the top/left
+ if (weightSum > 0.0f) {
+ float weight = getWeight(child);
+
+ // We can't emulate a LinearLayout where multiple children have positive
+ // weights. However, we CAN support the common scenario where a single
+ // child has a non-zero weight, and all children after it are pushed
+ // to the end and the weighted child fills the remaining space.
+ if (cumulativeWeight == 0 && weight > 0) {
+ // See if we have a bottom/right edge to attach the forwards link to
+ // (at the end of the forwards chains). Only if so can we link forwards.
+ View referenced;
+ if (isVertical) {
+ referenced = edgeList.getSharedBottomEdge(layout);
+ } else {
+ referenced = edgeList.getSharedRightEdge(layout);
+ }
+ if (referenced != null) {
+ linkForwards = true;
+ }
+ } else if (cumulativeWeight > 0) {
+ linkBackwards = false;
+ }
+
+ cumulativeWeight += weight;
+ }
+
+ analyzeGravity(edgeList, layout, isVertical, child, childView);
+ convert0dipToWrapContent(child);
+
+ // Chain elements together in the flow direction of the linear layout
+ if (prevId != null) { // No constraint for first child
+ if (linkBackwards) {
+ if (isVertical) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId);
+ }
+ }
+ } else if (isFirstChild) {
+ assert linkBackwards;
+
+ // First element; attach it to the parent if we can
+ if (isVertical) {
+ View referenced = edgeList.getSharedTopEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced.getId());
+ }
+ }
+ } else {
+ View referenced = edgeList.getSharedLeftEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
+ }
+ }
+ }
+ }
+
+ if (linkForwards) {
+ if (index < (childCount - 1)) {
+ Element nextChild = children.get(index + 1);
+ String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null);
+ if (nextId != null) {
+ if (isVertical) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId);
+ }
+ }
+ } else {
+ // Attach to right/bottom edge of the layout
+ if (isVertical) {
+ View referenced = edgeList.getSharedBottomEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced.getId());
+ }
+ }
+ } else {
+ View referenced = edgeList.getSharedRightEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
+ }
+ }
+ }
+ }
+ }
+
+ if (baselineRef != null && baselineRef.getId() != null
+ && !baselineRef.getId().equals(childView.getId())) {
+ assert !isVertical;
+ // Only align if they share the same gravity
+ if ((childView.getGravity() & GRAVITY_VERT_MASK) ==
+ (baselineRef.getGravity() & GRAVITY_VERT_MASK)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId());
+ }
+ }
+
+ prevId = mRefactoring.ensureHasId(mRootEdit, child, null);
+ isFirstChild = false;
+ }
+ }
+
+ /**
+ * Checks the layout "gravity" value for the given child and updates the constraints
+ * to account for the gravity
+ */
+ private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical,
+ Element child, View childView) {
+ // Use gravity to constrain elements in the axis orthogonal to the
+ // direction of the layout
+ int gravity = childView.getGravity();
+ if (isVertical) {
+ if ((gravity & GRAVITY_RIGHT) != 0) {
+ View referenced = edgeList.getSharedRightEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
+ referenced.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) {
+ View referenced1 = edgeList.getSharedLeftEdge(layout);
+ View referenced2 = edgeList.getSharedRightEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL,
+ VALUE_TRUE);
+ }
+ }
+ } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) {
+ View referenced1 = edgeList.getSharedLeftEdge(layout);
+ View referenced2 = edgeList.getSharedRightEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ VALUE_TRUE);
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
+ referenced1.getId());
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
+ referenced2.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_LEFT) != 0) {
+ View referenced = edgeList.getSharedLeftEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
+ referenced.getId());
+ }
+ }
+ }
+ } else {
+ // Handle horizontal layout: perform vertical gravity attachments
+ if ((gravity & GRAVITY_BOTTOM) != 0) {
+ View referenced = edgeList.getSharedBottomEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_CENTER_VERT) != 0) {
+ View referenced1 = edgeList.getSharedTopEdge(layout);
+ View referenced2 = edgeList.getSharedBottomEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL,
+ VALUE_TRUE);
+ }
+ }
+ } else if ((gravity & GRAVITY_FILL_VERT) != 0) {
+ View referenced1 = edgeList.getSharedTopEdge(layout);
+ View referenced2 = edgeList.getSharedBottomEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ VALUE_TRUE);
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced1.getId());
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced2.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_TOP) != 0) {
+ View referenced = edgeList.getSharedTopEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced.getId());
+ }
+ }
+ }
+ }
+ return gravity;
+ }
+
+ /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
+ private void convert0dipToWrapContent(Element child) {
+ // Must convert layout_height="0dip" to layout_height="wrap_content".
+ // 0dip is a special trick used in linear layouts in the presence of
+ // weights where 0dip ensures that the height of the view is not taken
+ // into account when distributing the weights. However, when converted
+ // to RelativeLayout this will instead cause the view to actually be assigned
+ // 0 height.
+ String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ // 0dip, 0dp, 0px, etc
+ if (height != null && height.startsWith("0")) { //$NON-NLS-1$
+ mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
+ mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT,
+ VALUE_WRAP_CONTENT);
+ }
+ String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ if (width != null && width.startsWith("0")) { //$NON-NLS-1$
+ mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
+ mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH,
+ VALUE_WRAP_CONTENT);
+ }
+ }
+
+ /**
+ * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the
+ * constraints in the EdgeList with those relationships which can continue in the
+ * outer single RelativeLayout.
+ */
+ private void analyzeRelativeLayout(EdgeList edgeList, Element layout) {
+ NodeList children = layout.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ View childView = edgeList.getView(child);
+ if (childView == null) {
+ // Could be a nested layout that is being removed etc
+ continue;
+ }
+
+ NamedNodeMap attributes = child.getAttributes();
+ for (int j = 0, m = attributes.getLength(); j < m; j++) {
+ Attr attribute = (Attr) attributes.item(j);
+ String name = attribute.getLocalName();
+ String value = attribute.getValue();
+ if (name.equals(ATTR_LAYOUT_WIDTH)
+ || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Ignore these for now
+ } else if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // Determine if the reference is to a known edge
+ String id = getIdBasename(value);
+ if (id != null) {
+ View referenced = edgeList.getView(id);
+ if (referenced != null) {
+ // This is a valid reference, so preserve
+ // the attribute
+ if (name.equals(ATTR_LAYOUT_BELOW) ||
+ name.equals(ATTR_LAYOUT_ABOVE) ||
+ name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
+ name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
+ name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) {
+ // Vertical constraint
+ childView.addVerticalConstraint(name, value);
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
+ name.equals(ATTR_LAYOUT_TO_LEFT_OF) ||
+ name.equals(ATTR_LAYOUT_TO_RIGHT_OF) ||
+ name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) {
+ // Horizontal constraint
+ childView.addHorizConstraint(name, value);
+ } else {
+ // We don't expect this
+ assert false : name;
+ }
+ } else {
+ // Reference to some layout that is not included here.
+ // TODO: See if the given layout has an edge
+ // that corresponds to one of our known views
+ // so we can adjust the constraints and keep it after all.
+ }
+ } else {
+ // It's a parent-relative constraint (such
+ // as aligning with a parent edge, or centering
+ // in the parent view)
+ boolean remove = true;
+ if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
+ View referenced = edgeList.getSharedLeftEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
+ }
+ remove = false;
+ }
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
+ View referenced = edgeList.getSharedRightEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
+ }
+ remove = false;
+ }
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
+ View referenced = edgeList.getSharedTopEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced.getId());
+ }
+ remove = false;
+ }
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
+ View referenced = edgeList.getSharedBottomEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced.getId());
+ }
+ remove = false;
+ }
+ }
+
+ boolean alignWithParent =
+ name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING);
+ if (remove && alignWithParent) {
+ // TODO - look for this one AFTER we have processed
+ // everything else, and then set constraints as necessary
+ // IF there are no other conflicting constraints!
+ }
+
+ // Otherwise it's some kind of centering which we don't support
+ // yet.
+
+ // TODO: Find a way to determine whether we have
+ // a corresponding edge for the parent (e.g. if
+ // the ViewInfo bounds match our outer parent or
+ // some other edge) and if so, substitute for that
+ // id.
+ // For example, if this element was centered
+ // horizontally in a RelativeLayout that actually
+ // occupies the entire width of our outer layout,
+ // then it can be preserved after all!
+
+ if (remove) {
+ if (name.startsWith("layout_margin")) { //$NON-NLS-1$
+ continue;
+ }
+
+ // Remove unknown attributes?
+ // It's too early to do this, because we may later want
+ // to *set* this value and it would result in an overlapping edits
+ // exception. Therefore, we need to RECORD which attributes should
+ // be removed, which lines should have its indentation adjusted
+ // etc and finally process it all at the end!
+ //mRefactoring.removeAttribute(mRootEdit, child,
+ // attribute.getNamespaceURI(), name);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will
+ * return null.
+ */
+ private static String getIdBasename(String id) {
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ return id.substring(NEW_ID_PREFIX.length());
+ } else if (id.startsWith(ID_PREFIX)) {
+ return id.substring(ID_PREFIX.length());
+ }
+
+ return null;
+ }
+
+ /** Returns true if the given second argument is a descendant of the first argument */
+ private static boolean isAncestor(Node ancestor, Node node) {
+ while (node != null) {
+ if (node == ancestor) {
+ return true;
+ }
+ node = node.getParentNode();
+ }
+ return false;
+ }
+
+ /**
+ * Computes horizontal constraints for the views in the grid for any remaining views
+ * that do not have constraints (as the result of the analysis of known layouts). This
+ * will look at the rendered layout coordinates and attempt to connect elements based
+ * on a spatial layout in the grid.
+ */
+ private void computeHorizontalConstraints(Grid grid) {
+ int columns = grid.getColumns();
+
+ String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+ String attachLeftValue = VALUE_TRUE;
+ int marginLeft = 0;
+ for (int col = 0; col < columns; col++) {
+ if (!grid.colContainsTopLeftCorner(col)) {
+ // Just accumulate margins for the next column
+ marginLeft += grid.getColumnWidth(col);
+ } else {
+ // Add horizontal attachments
+ String firstId = null;
+ for (View view : grid.viewsStartingInCol(col, true)) {
+ assert view.getId() != null;
+ if (firstId == null) {
+ firstId = view.getId();
+ if (view.isConstrainedHorizontally()) {
+ // Nothing to do -- we already have an accurate position for
+ // this view
+ } else if (attachLeftProperty != null) {
+ view.addHorizConstraint(attachLeftProperty, attachLeftValue);
+ if (marginLeft > 0) {
+ view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT,
+ String.format(VALUE_N_DP, marginLeft));
+ marginLeft = 0;
+ }
+ } else {
+ assert false;
+ }
+ } else if (!view.isConstrainedHorizontally()) {
+ view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId);
+ }
+ }
+ }
+
+ // Figure out edge for the next column
+ View view = grid.findRightEdgeView(col);
+ if (view != null) {
+ assert view.getId() != null;
+ attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF;
+ attachLeftValue = view.getId();
+
+ marginLeft = 0;
+ } else if (marginLeft == 0) {
+ marginLeft = grid.getColumnWidth(col);
+ }
+ }
+ }
+
+ /**
+ * Performs vertical layout just like the {@link #computeHorizontalConstraints} method
+ * did horizontally
+ */
+ private void computeVerticalConstraints(Grid grid) {
+ int rows = grid.getRows();
+
+ String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP;
+ String attachTopValue = VALUE_TRUE;
+ int marginTop = 0;
+ for (int row = 0; row < rows; row++) {
+ if (!grid.rowContainsTopLeftCorner(row)) {
+ // Just accumulate margins for the next column
+ marginTop += grid.getRowHeight(row);
+ } else {
+ // Add horizontal attachments
+ String firstId = null;
+ for (View view : grid.viewsStartingInRow(row, true)) {
+ assert view.getId() != null;
+ if (firstId == null) {
+ firstId = view.getId();
+ if (view.isConstrainedVertically()) {
+ // Nothing to do -- we already have an accurate position for
+ // this view
+ } else if (attachTopProperty != null) {
+ view.addVerticalConstraint(attachTopProperty, attachTopValue);
+ if (marginTop > 0) {
+ view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP,
+ String.format(VALUE_N_DP, marginTop));
+ marginTop = 0;
+ }
+ } else {
+ assert false;
+ }
+ } else if (!view.isConstrainedVertically()) {
+ view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId);
+ }
+ }
+ }
+
+ // Figure out edge for the next row
+ View view = grid.findBottomEdgeView(row);
+ if (view != null) {
+ assert view.getId() != null;
+ attachTopProperty = ATTR_LAYOUT_BELOW;
+ attachTopValue = view.getId();
+ marginTop = 0;
+ } else if (marginTop == 0) {
+ marginTop = grid.getRowHeight(row);
+ }
+ }
+ }
+
+ /**
+ * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
+ * {@link Element}
+ *
+ * @param info the root {@link CanvasViewInfo} to search below
+ * @param element the target element
+ * @return the {@link CanvasViewInfo} which corresponds to the given element
+ */
+ private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
+ if (getElement(info) == element) {
+ return info;
+ }
+
+ for (CanvasViewInfo child : info.getChildren()) {
+ CanvasViewInfo result = findViewForElement(child, element);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
+ private static Element getElement(CanvasViewInfo info) {
+ Node node = info.getUiViewNode().getXmlNode();
+ if (node instanceof Element) {
+ return (Element) node;
+ }
+
+ return null;
+ }
+
+ /**
+ * A grid of cells which can contain views, used to infer spatial relationships when
+ * computing constraints. Note that a view can appear in than one cell; they will
+ * appear in all cells that their bounds overlap with!
+ */
+ private class Grid {
+ private final int[] mLeft;
+ private final int[] mTop;
+ // A list from row to column to cell, where a cell is a list of views
+ private final List<List<List<View>>> mRowList;
+ private int mRowCount;
+ private int mColCount;
+
+ Grid(List<View> views, int[] left, int[] top) {
+ mLeft = left;
+ mTop = top;
+
+ // The left/top arrays should include the ending point too
+ mColCount = left.length - 1;
+ mRowCount = top.length - 1;
+
+ // Using nested lists rather than arrays to avoid lack of typed arrays
+ // (can't create List<View>[row][column] arrays)
+ mRowList = new ArrayList<List<List<View>>>(top.length);
+ for (int row = 0; row < top.length; row++) {
+ List<List<View>> columnList = new ArrayList<List<View>>(left.length);
+ for (int col = 0; col < left.length; col++) {
+ columnList.add(new ArrayList<View>(4));
+ }
+ mRowList.add(columnList);
+ }
+
+ for (View view : views) {
+ // Get rid of the root view; we don't want that in the attachments logic;
+ // it was there originally such that it would contribute the outermost
+ // edges.
+ if (view.mElement == mLayout) {
+ continue;
+ }
+
+ for (int i = 0; i < view.mRowSpan; i++) {
+ for (int j = 0; j < view.mColSpan; j++) {
+ mRowList.get(view.mRow + i).get(view.mCol + j).add(view);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the number of rows in the grid
+ *
+ * @return the row count
+ */
+ public int getRows() {
+ return mRowCount;
+ }
+
+ /**
+ * Returns the number of columns in the grid
+ *
+ * @return the column count
+ */
+ public int getColumns() {
+ return mColCount;
+ }
+
+ /**
+ * Returns the list of views overlapping the given cell
+ *
+ * @param row the row of the target cell
+ * @param col the column of the target cell
+ * @return a list of views overlapping the given column
+ */
+ public List<View> get(int row, int col) {
+ return mRowList.get(row).get(col);
+ }
+
+ /**
+ * Returns true if the given column contains a top left corner of a view
+ *
+ * @param column the column to check
+ * @return true if one or more views have their top left corner in this column
+ */
+ public boolean colContainsTopLeftCorner(int column) {
+ for (int row = 0; row < mRowCount; row++) {
+ View view = getTopLeftCorner(row, column);
+ if (view != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the given row contains a top left corner of a view
+ *
+ * @param row the row to check
+ * @return true if one or more views have their top left corner in this row
+ */
+ public boolean rowContainsTopLeftCorner(int row) {
+ for (int col = 0; col < mColCount; col++) {
+ View view = getTopLeftCorner(row, col);
+ if (view != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a list of views (optionally sorted by increasing row index) that have
+ * their left edge starting in the given column
+ *
+ * @param col the column to look up views for
+ * @param sort whether to sort the result in increasing row order
+ * @return a list of views starting in the given column
+ */
+ public List<View> viewsStartingInCol(int col, boolean sort) {
+ List<View> views = new ArrayList<View>();
+ for (int row = 0; row < mRowCount; row++) {
+ View view = getTopLeftCorner(row, col);
+ if (view != null) {
+ views.add(view);
+ }
+ }
+
+ if (sort) {
+ View.sortByRow(views);
+ }
+
+ return views;
+ }
+
+ /**
+ * Returns a list of views (optionally sorted by increasing column index) that have
+ * their top edge starting in the given row
+ *
+ * @param row the row to look up views for
+ * @param sort whether to sort the result in increasing column order
+ * @return a list of views starting in the given row
+ */
+ public List<View> viewsStartingInRow(int row, boolean sort) {
+ List<View> views = new ArrayList<View>();
+ for (int col = 0; col < mColCount; col++) {
+ View view = getTopLeftCorner(row, col);
+ if (view != null) {
+ views.add(view);
+ }
+ }
+
+ if (sort) {
+ View.sortByColumn(views);
+ }
+
+ return views;
+ }
+
+ /**
+ * Returns the pixel width of the given column
+ *
+ * @param col the column to look up the width of
+ * @return the width of the column
+ */
+ public int getColumnWidth(int col) {
+ return mLeft[col + 1] - mLeft[col];
+ }
+
+ /**
+ * Returns the pixel height of the given row
+ *
+ * @param row the row to look up the height of
+ * @return the height of the row
+ */
+ public int getRowHeight(int row) {
+ return mTop[row + 1] - mTop[row];
+ }
+
+ /**
+ * Returns the first view found that has its top left corner in the cell given by
+ * the row and column indexes, or null if not found.
+ *
+ * @param row the row of the target cell
+ * @param col the column of the target cell
+ * @return a view with its top left corner in the given cell, or null if not found
+ */
+ View getTopLeftCorner(int row, int col) {
+ List<View> views = get(row, col);
+ if (views.size() > 0) {
+ for (View view : views) {
+ if (view.mRow == row && view.mCol == col) {
+ return view;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public View findRightEdgeView(int col) {
+ for (int row = 0; row < mRowCount; row++) {
+ List<View> views = get(row, col);
+ if (views.size() > 0) {
+ List<View> result = new ArrayList<View>();
+ for (View view : views) {
+ // Ends on the right edge of this column?
+ if (view.mCol + view.mColSpan == col + 1) {
+ result.add(view);
+ }
+ }
+ if (result.size() > 1) {
+ View.sortByColumn(result);
+ }
+ if (result.size() > 0) {
+ return result.get(0);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public View findBottomEdgeView(int row) {
+ for (int col = 0; col < mColCount; col++) {
+ List<View> views = get(row, col);
+ if (views.size() > 0) {
+ List<View> result = new ArrayList<View>();
+ for (View view : views) {
+ // Ends on the bottom edge of this column?
+ if (view.mRow + view.mRowSpan == row + 1) {
+ result.add(view);
+ }
+ }
+ if (result.size() > 1) {
+ View.sortByRow(result);
+ }
+ if (result.size() > 0) {
+ return result.get(0);
+ }
+
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Produces a display of view contents along with the pixel positions of each row/column,
+ * like the following (used for diagnostics only)
+ * <pre>
+ * |0 |49 |143 |192 |240
+ * 36| | |button2 |
+ * 72| |radioButton1 |button2 |
+ * 74|button1 |radioButton1 |button2 |
+ * 108|button1 | |button2 |
+ * 110| | |button2 |
+ * 149| | | |
+ * 320
+ * </pre>
+ */
+ @Override
+ public String toString() {
+ // Dump out the view table
+ int cellWidth = 20;
+
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter out = new PrintWriter(stringWriter);
+ out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ for (int col = 0; col < mColCount + 1; col++) {
+ out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ out.printf("\n"); //$NON-NLS-1$
+ for (int row = 0; row < mRowCount + 1; row++) {
+ out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$
+ if (row == mRowCount) {
+ break;
+ }
+ for (int col = 0; col < mColCount; col++) {
+ List<View> views = get(row, col);
+ StringBuilder sb = new StringBuilder();
+ for (View view : views) {
+ String id = view != null ? view.getId() : ""; //$NON-NLS-1$
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ id = id.substring(NEW_ID_PREFIX.length());
+ }
+ if (id.length() > cellWidth - 2) {
+ id = id.substring(0, cellWidth - 2);
+ }
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ sb.append(id);
+ }
+ String cellString = sb.toString();
+ if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$
+ cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$
+ }
+ out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ out.printf("\n"); //$NON-NLS-1$
+ }
+
+ out.flush();
+ return stringWriter.toString();
+ }
+ }
+
+ /** Holds layout information about an individual view. */
+ private static class View {
+ private final Element mElement;
+ private int mRow = -1;
+ private int mCol = -1;
+ private int mRowSpan = -1;
+ private int mColSpan = -1;
+ private CanvasViewInfo mInfo;
+ private String mId;
+ private List<Pair<String, String>> mHorizConstraints =
+ new ArrayList<Pair<String, String>>(4);
+ private List<Pair<String, String>> mVerticalConstraints =
+ new ArrayList<Pair<String, String>>(4);
+ private int mGravity;
+
+ public View(CanvasViewInfo view, Element element) {
+ mInfo = view;
+ mElement = element;
+ mGravity = GravityHelper.getGravity(element);
+ }
+
+ public int getHeight() {
+ return mInfo.getAbsRect().height;
+ }
+
+ public int getGravity() {
+ return mGravity;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public Element getElement() {
+ return mElement;
+ }
+
+ public List<Pair<String, String>> getHorizConstraints() {
+ return mHorizConstraints;
+ }
+
+ public List<Pair<String, String>> getVerticalConstraints() {
+ return mVerticalConstraints;
+ }
+
+ public boolean isConstrainedHorizontally() {
+ return mHorizConstraints.size() > 0;
+ }
+
+ public boolean isConstrainedVertically() {
+ return mVerticalConstraints.size() > 0;
+ }
+
+ public void addHorizConstraint(String property, String value) {
+ assert property != null && value != null;
+ // TODO - look for duplicates?
+ mHorizConstraints.add(Pair.of(property, value));
+ }
+
+ public void addVerticalConstraint(String property, String value) {
+ assert property != null && value != null;
+ mVerticalConstraints.add(Pair.of(property, value));
+ }
+
+ public int getLeftEdge() {
+ return mInfo.getAbsRect().x;
+ }
+
+ public int getTopEdge() {
+ return mInfo.getAbsRect().y;
+ }
+
+ public int getRightEdge() {
+ Rectangle bounds = mInfo.getAbsRect();
+ // +1: make the bounds overlap, so the right edge is the same as the
+ // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide
+ // columns between adjacent items.
+ return bounds.x + bounds.width + 1;
+ }
+
+ public int getBottomEdge() {
+ Rectangle bounds = mInfo.getAbsRect();
+ return bounds.y + bounds.height + 1;
+ }
+
+ @Override
+ public String toString() {
+ return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ public static void sortByRow(List<View> views) {
+ Collections.sort(views, new ViewComparator(true/*rowSort*/));
+ }
+
+ public static void sortByColumn(List<View> views) {
+ Collections.sort(views, new ViewComparator(false/*rowSort*/));
+ }
+
+ /** Comparator to help sort views by row or column index */
+ private static class ViewComparator implements Comparator<View> {
+ boolean mRowSort;
+
+ public ViewComparator(boolean rowSort) {
+ mRowSort = rowSort;
+ }
+
+ @Override
+ public int compare(View view1, View view2) {
+ if (mRowSort) {
+ return view1.mRow - view2.mRow;
+ } else {
+ return view1.mCol - view2.mCol;
+ }
+ }
+ }
+ }
+
+ /**
+ * An edge list takes a hierarchy of elements and records the bounds of each element
+ * into various lists such that it can answer queries about shared edges, about which
+ * particular pixels occur as a boundary edge, etc.
+ */
+ private class EdgeList {
+ private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100);
+ private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100);
+ private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>();
+ private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>();
+ private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>();
+ private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>();
+ private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>();
+ private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>();
+ private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>();
+ private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>();
+ private final List<Element> mDelete = new ArrayList<Element>();
+
+ EdgeList(CanvasViewInfo view) {
+ analyze(view, true);
+ mDelete.remove(getElement(view));
+ }
+
+ public void setIdAttributeValue(View view, String id) {
+ assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX);
+ view.mId = id;
+ mIdToViewMap.put(getIdBasename(id), view);
+ }
+
+ public View getView(Element element) {
+ return mElementToViewMap.get(element);
+ }
+
+ public View getView(String id) {
+ return mIdToViewMap.get(id);
+ }
+
+ public List<View> getTopEdgeViews(Integer topOffset) {
+ return mTop.get(topOffset);
+ }
+
+ public List<View> getLeftEdgeViews(Integer leftOffset) {
+ return mLeft.get(leftOffset);
+ }
+
+ void record(Map<Integer, List<View>> map, Integer edge, View info) {
+ List<View> list = map.get(edge);
+ if (list == null) {
+ list = new ArrayList<View>();
+ map.put(edge, list);
+ }
+ list.add(info);
+ }
+
+ private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) {
+ Set<Integer> joined = new HashSet<Integer>(first.size() + second.size());
+ joined.addAll(first);
+ joined.addAll(second);
+ List<Integer> unique = new ArrayList<Integer>(joined);
+ Collections.sort(unique);
+
+ return unique;
+ }
+
+ public List<Element> getDeletedElements() {
+ return mDelete;
+ }
+
+ public List<Integer> getColumnOffsets() {
+ return getOffsets(mLeft.keySet(), mRight.keySet());
+ }
+ public List<Integer> getRowOffsets() {
+ return getOffsets(mTop.keySet(), mBottom.keySet());
+ }
+
+ private View analyze(CanvasViewInfo view, boolean isRoot) {
+ View added = null;
+ if (!mFlatten || !isRemovableLayout(view)) {
+ added = add(view);
+ if (!isRoot) {
+ return added;
+ }
+ } else {
+ mDelete.add(getElement(view));
+ }
+
+ Element parentElement = getElement(view);
+ Rectangle parentBounds = view.getAbsRect();
+
+ // Build up a table model of the view
+ for (CanvasViewInfo child : view.getChildren()) {
+ Rectangle childBounds = child.getAbsRect();
+ Element childElement = getElement(child);
+
+ // See if this view shares the edge with the removed
+ // parent layout, and if so, record that such that we can
+ // later handle attachments to the removed parent edges
+ if (parentBounds.x == childBounds.x) {
+ mSharedLeftEdge.put(childElement, parentElement);
+ }
+ if (parentBounds.y == childBounds.y) {
+ mSharedTopEdge.put(childElement, parentElement);
+ }
+ if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) {
+ mSharedRightEdge.put(childElement, parentElement);
+ }
+ if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) {
+ mSharedBottomEdge.put(childElement, parentElement);
+ }
+
+ if (mFlatten && isRemovableLayout(child)) {
+ // When flattening, we want to disregard all layouts and instead
+ // add their children!
+ for (CanvasViewInfo childView : child.getChildren()) {
+ analyze(childView, false);
+
+ Element childViewElement = getElement(childView);
+ Rectangle childViewBounds = childView.getAbsRect();
+
+ // See if this view shares the edge with the removed
+ // parent layout, and if so, record that such that we can
+ // later handle attachments to the removed parent edges
+ if (parentBounds.x == childViewBounds.x) {
+ mSharedLeftEdge.put(childViewElement, parentElement);
+ }
+ if (parentBounds.y == childViewBounds.y) {
+ mSharedTopEdge.put(childViewElement, parentElement);
+ }
+ if (parentBounds.x + parentBounds.width == childViewBounds.x
+ + childViewBounds.width) {
+ mSharedRightEdge.put(childViewElement, parentElement);
+ }
+ if (parentBounds.y + parentBounds.height == childViewBounds.y
+ + childViewBounds.height) {
+ mSharedBottomEdge.put(childViewElement, parentElement);
+ }
+ }
+ mDelete.add(childElement);
+ } else {
+ analyze(child, false);
+ }
+ }
+
+ return added;
+ }
+
+ public View getSharedLeftEdge(Element element) {
+ return getSharedEdge(element, mSharedLeftEdge);
+ }
+
+ public View getSharedRightEdge(Element element) {
+ return getSharedEdge(element, mSharedRightEdge);
+ }
+
+ public View getSharedTopEdge(Element element) {
+ return getSharedEdge(element, mSharedTopEdge);
+ }
+
+ public View getSharedBottomEdge(Element element) {
+ return getSharedEdge(element, mSharedBottomEdge);
+ }
+
+ private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) {
+ Element original = element;
+
+ while (element != null) {
+ View view = getView(element);
+ if (view != null) {
+ assert isAncestor(element, original);
+ return view;
+ }
+ element = sharedEdgeMap.get(element);
+ }
+
+ return null;
+ }
+
+ private View add(CanvasViewInfo info) {
+ Rectangle bounds = info.getAbsRect();
+ Element element = getElement(info);
+ View view = new View(info, element);
+ mElementToViewMap.put(element, view);
+ record(mLeft, Integer.valueOf(bounds.x), view);
+ record(mTop, Integer.valueOf(bounds.y), view);
+ record(mRight, Integer.valueOf(view.getRightEdge()), view);
+ record(mBottom, Integer.valueOf(view.getBottomEdge()), view);
+ return view;
+ }
+
+ /**
+ * Returns true if the given {@link CanvasViewInfo} represents an element we
+ * should remove in a flattening conversion. We don't want to remove non-layout
+ * views, or layout views that for example contain drawables on their own.
+ */
+ private boolean isRemovableLayout(CanvasViewInfo child) {
+ // The element being converted is NOT removable!
+ Element element = getElement(child);
+ if (element == mLayout) {
+ return false;
+ }
+
+ ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
+ String name = descriptor.getXmlLocalName();
+ if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)) {
+ // Don't delete layouts that provide a background image or gradient
+ if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
+ AdtPlugin.log(IStatus.WARNING,
+ "Did not flatten layout %1$s because it defines a '%2$s' attribute",
+ VisualRefactoring.getId(element), ATTR_BACKGROUND);
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapAction.java
new file mode 100644
index 000000000..02c2a276c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapAction.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Remove Container" menu item is invoked.
+ */
+public class UnwrapAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ UnwrapRefactoring ref = new UnwrapRefactoring(mFile, mDelegate,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new UnwrapWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Remove Container...", editorDelegate, UnwrapAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapContribution.java
new file mode 100644
index 000000000..0869fd637
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapContribution.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class UnwrapContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new UnwrapRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof UnwrapRefactoring.Descriptor) {
+ return ((UnwrapRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java
new file mode 100644
index 000000000..4eff2cde5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.EXT_XML;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Removes the layout surrounding the current selection (or if the current selection has
+ * children, removes the current layout), and migrates namespace and layout attributes.
+ */
+@SuppressWarnings("restriction") // XML model
+public class UnwrapRefactoring extends VisualRefactoring {
+ private Element mContainer;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ UnwrapRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ }
+
+ public UnwrapRefactoring(
+ IFile file,
+ LayoutEditorDelegate delegate,
+ ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, delegate, selection, treeSelection);
+ }
+
+ @VisibleForTesting
+ UnwrapRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
+ super(selectedElements, editor);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to wrap");
+ return status;
+ }
+
+ // Make sure that the selection all has the same parent?
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to unwrap");
+ return status;
+ }
+
+ Element first = mElements.get(0);
+
+ // Determine the element of the container to be removed.
+ // If you've selected a non-container, or you've selected multiple
+ // elements, then it's the parent which should be removed. Otherwise,
+ // it's the selection itself which represents the container.
+ boolean useParent = mElements.size() > 1;
+ if (!useParent) {
+ if (DomUtilities.getChildren(first).size() == 0) {
+ useParent = true;
+ }
+ }
+ Node parent = first.getParentNode();
+ if (parent instanceof Document) {
+ mContainer = first;
+ List<Element> elements = DomUtilities.getChildren(mContainer);
+ if (elements.size() == 0) {
+ status.addFatalError(
+ "Cannot remove container when it has no children");
+ return status;
+ }
+ } else if (useParent && (parent instanceof Element)) {
+ mContainer = (Element) parent;
+ } else {
+ mContainer = first;
+ }
+
+ for (Element element : mElements) {
+ if (element.getParentNode() != parent) {
+ status.addFatalError(
+ "All unwrapped elements must share the same parent element");
+ return status;
+ }
+ }
+
+ // Ensure that if we are removing the root, that it has only one child
+ // such that there is a new single root
+ if (mContainer.getParentNode() instanceof Document) {
+ if (DomUtilities.getChildren(mContainer).size() > 1) {
+ status.addFatalError(
+ "Cannot remove root: it has more than one child "
+ + "which would result in multiple new roots");
+ return status;
+ }
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ public String getName() {
+ return "Remove Container";
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ // (1) If the removed parent is the root container, transfer its
+ // namespace declarations
+ // (2) Remove the root element completely
+ // (3) Transfer layout attributes?
+ // (4) Check for Java R.file usages?
+
+ IFile file = mDelegate.getEditor().getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ if (file == null) {
+ return changes;
+ }
+ MultiTextEdit rootEdit = new MultiTextEdit();
+
+ // Transfer namespace elements?
+ if (mContainer.getParentNode() instanceof Document) {
+ List<Element> elements = DomUtilities.getChildren(mContainer);
+ assert elements.size() == 1;
+ Element newRoot = elements.get(0);
+
+ List<Attr> declarations = findNamespaceAttributes(mContainer);
+ for (Attr attribute : declarations) {
+ if (attribute instanceof IndexedRegion) {
+ setAttribute(rootEdit, newRoot, attribute.getNamespaceURI(),
+ attribute.getPrefix(), attribute.getLocalName(), attribute.getValue());
+ }
+ }
+ }
+
+ // Transfer layout_ attributes (other than width and height)
+ List<Element> children = DomUtilities.getChildren(mContainer);
+ if (children.size() == 1) {
+ List<Attr> layoutAttributes = findLayoutAttributes(mContainer);
+ for (Attr attribute : layoutAttributes) {
+ String name = attribute.getLocalName();
+ if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // Already handled specially
+ continue;
+ }
+ }
+ }
+
+ // Remove the root
+ removeElementTags(rootEdit, mContainer, Collections.<Element>emptyList() /* skip */,
+ false /*changeIndentation*/);
+
+ MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ change.setEdit(rootEdit);
+ change.setTextType(EXT_XML);
+ changes.add(change);
+ return changes;
+ }
+
+ @Override
+ public VisualRefactoringWizard createWizard() {
+ return new UnwrapWizard(this, mDelegate);
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.unwrap", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new UnwrapRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapWizard.java
new file mode 100644
index 000000000..6e3bcf1e7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapWizard.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+public class UnwrapWizard extends VisualRefactoringWizard {
+ public UnwrapWizard(UnwrapRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle("Remove Container");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ // This refactoring takes no parameters
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableAction.java
new file mode 100644
index 000000000..84d3e7ee8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableAction.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Convert Layout" menu item is invoked.
+ */
+public class UseCompoundDrawableAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ UseCompoundDrawableRefactoring ref = new UseCompoundDrawableRefactoring(
+ mFile, mDelegate, mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new UseCompoundDrawableWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Convert to a Compound Drawable...", editorDelegate,
+ UseCompoundDrawableAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java
new file mode 100644
index 000000000..0e56bdf4d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_DRAWABLE_BOTTOM;
+import static com.android.SdkConstants.ATTR_DRAWABLE_LEFT;
+import static com.android.SdkConstants.ATTR_DRAWABLE_PADDING;
+import static com.android.SdkConstants.ATTR_DRAWABLE_RIGHT;
+import static com.android.SdkConstants.ATTR_DRAWABLE_TOP;
+import static com.android.SdkConstants.ATTR_GRAVITY;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.ATTR_SRC;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.IMAGE_VIEW;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.TEXT_VIEW;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Converts a LinearLayout with exactly a TextView child and an ImageView child into
+ * a single TextView with a compound drawable.
+ */
+@SuppressWarnings("restriction") // XML model
+public class UseCompoundDrawableRefactoring extends VisualRefactoring {
+ /**
+ * Constructs a new {@link UseCompoundDrawableRefactoring}
+ *
+ * @param file the file to refactor in
+ * @param editor the corresponding editor
+ * @param selection the editor selection, or null
+ * @param treeSelection the canvas selection, or null
+ */
+ public UseCompoundDrawableRefactoring(IFile file, LayoutEditorDelegate editor,
+ ITextSelection selection, ITreeSelection treeSelection) {
+ super(file, editor, selection, treeSelection);
+ }
+
+ /**
+ * This constructor is solely used by {@link Descriptor}, to replay a
+ * previous refactoring.
+ *
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ private UseCompoundDrawableRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ }
+
+ @VisibleForTesting
+ UseCompoundDrawableRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
+ super(selectedElements, editor);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("Nothing to convert");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+
+ // Enforce that the selection is -contiguous-
+ if (!validateContiguous(infos, status)) {
+ return status;
+ }
+ }
+
+ // Ensures that we have a valid DOM model:
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to convert");
+ return status;
+ }
+
+ // Ensure that we have selected precisely one LinearLayout
+ if (mElements.size() != 1 ||
+ !(mElements.get(0).getTagName().equals(LINEAR_LAYOUT))) {
+ status.addFatalError("Must select exactly one LinearLayout");
+ return status;
+ }
+
+ Element layout = mElements.get(0);
+ List<Element> children = DomUtilities.getChildren(layout);
+ if (children.size() != 2) {
+ status.addFatalError("The LinearLayout must have exactly two children");
+ return status;
+ }
+ Element first = children.get(0);
+ Element second = children.get(1);
+ boolean haveTextView =
+ first.getTagName().equals(TEXT_VIEW)
+ || second.getTagName().equals(TEXT_VIEW);
+ boolean haveImageView =
+ first.getTagName().equals(IMAGE_VIEW)
+ || second.getTagName().equals(IMAGE_VIEW);
+ if (!(haveTextView && haveImageView)) {
+ status.addFatalError("The LinearLayout must have exactly one TextView child " +
+ "and one ImageView child");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ return super.createArgumentMap();
+ }
+
+ @Override
+ public String getName() {
+ return "Convert to Compound Drawable";
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ String androidNsPrefix = getAndroidNamespacePrefix();
+ IFile file = mDelegate.getEditor().getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ if (file == null) {
+ return changes;
+ }
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setTextType(EXT_XML);
+
+ // (1) Build up the contents of the new TextView. This is identical
+ // to the old contents, but with the addition of a drawableTop/Left/Right/Bottom
+ // attribute (depending on the orientation and order), as well as any layout
+ // params from the LinearLayout.
+ // (2) Delete the linear layout and replace with the text view.
+ // (3) Reformat.
+
+ // checkInitialConditions has already validated that we have exactly a LinearLayout
+ // with an ImageView and a TextView child (in either order)
+ Element layout = mElements.get(0);
+ List<Element> children = DomUtilities.getChildren(layout);
+ Element first = children.get(0);
+ Element second = children.get(1);
+ final Element text;
+ final Element image;
+ if (first.getTagName().equals(TEXT_VIEW)) {
+ text = first;
+ image = second;
+ } else {
+ text = second;
+ image = first;
+ }
+
+ // Horizontal is the default, so if no value is specified it is horizontal.
+ boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
+ ATTR_ORIENTATION));
+
+ // The WST DOM implementation doesn't correctly implement cloneNode: this returns
+ // an empty document instead:
+ // text.getOwnerDocument().cloneNode(false/*deep*/);
+ // Luckily we just need to clone a single element, not a nested structure, so it's
+ // easy enough to do this manually:
+ Document tempDocument = DomUtilities.createEmptyDocument();
+ if (tempDocument == null) {
+ return changes;
+ }
+ Element newTextElement = tempDocument.createElement(text.getTagName());
+ tempDocument.appendChild(newTextElement);
+
+ NamedNodeMap attributes = text.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ String name = attribute.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attribute.getNamespaceURI())
+ && !(name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))) {
+ // Ignore layout params: the parent layout is going away
+ } else {
+ newTextElement.setAttribute(attribute.getName(), attribute.getValue());
+ }
+ }
+
+ // Apply all layout params from the parent (except width and height),
+ // as well as android:gravity
+ List<Attr> layoutAttributes = findLayoutAttributes(layout);
+ for (Attr attribute : layoutAttributes) {
+ String name = attribute.getLocalName();
+ if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // Already handled specially
+ continue;
+ }
+ newTextElement.setAttribute(attribute.getName(), attribute.getValue());
+ }
+ String gravity = layout.getAttributeNS(ANDROID_URI, ATTR_GRAVITY);
+ if (gravity.length() > 0) {
+ setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_GRAVITY, gravity);
+ }
+
+ String src = image.getAttributeNS(ANDROID_URI, ATTR_SRC);
+
+ // Set the drawable
+ String drawableAttribute;
+ // The space between the image and the text can have margins/padding, both
+ // from the text's perspective and from the image's perspective. We need to
+ // combine these.
+ String padding1 = null;
+ String padding2 = null;
+ if (isVertical) {
+ if (first == image) {
+ drawableAttribute = ATTR_DRAWABLE_TOP;
+ padding1 = getPadding(image, ATTR_LAYOUT_MARGIN_BOTTOM);
+ padding2 = getPadding(text, ATTR_LAYOUT_MARGIN_TOP);
+ } else {
+ drawableAttribute = ATTR_DRAWABLE_BOTTOM;
+ padding1 = getPadding(text, ATTR_LAYOUT_MARGIN_BOTTOM);
+ padding2 = getPadding(image, ATTR_LAYOUT_MARGIN_TOP);
+ }
+ } else {
+ if (first == image) {
+ drawableAttribute = ATTR_DRAWABLE_LEFT;
+ padding1 = getPadding(image, ATTR_LAYOUT_MARGIN_RIGHT);
+ padding2 = getPadding(text, ATTR_LAYOUT_MARGIN_LEFT);
+ } else {
+ drawableAttribute = ATTR_DRAWABLE_RIGHT;
+ padding1 = getPadding(text, ATTR_LAYOUT_MARGIN_RIGHT);
+ padding2 = getPadding(image, ATTR_LAYOUT_MARGIN_LEFT);
+ }
+ }
+
+ setAndroidAttribute(newTextElement, androidNsPrefix, drawableAttribute, src);
+
+ String padding = combine(padding1, padding2);
+ if (padding != null) {
+ setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_DRAWABLE_PADDING, padding);
+ }
+
+ // If the removed LinearLayout is the root container, transfer its namespace
+ // declaration to the TextView
+ if (layout.getParentNode() instanceof Document) {
+ List<Attr> declarations = findNamespaceAttributes(layout);
+ for (Attr attribute : declarations) {
+ if (attribute instanceof IndexedRegion) {
+ newTextElement.setAttribute(attribute.getName(), attribute.getValue());
+ }
+ }
+ }
+
+ // Update any layout references to the layout to point to the text view
+ String layoutId = getId(layout);
+ if (layoutId.length() > 0) {
+ String id = getId(text);
+ if (id.length() == 0) {
+ id = ensureHasId(rootEdit, text, null, false);
+ setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_ID, id);
+ }
+
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
+ doc, mSelectionStart, mSelectionEnd, layoutId, id);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ String xml = EclipseXmlPrettyPrinter.prettyPrint(
+ tempDocument.getDocumentElement(),
+ EclipseXmlFormatPreferences.create(),
+ XmlFormatStyle.LAYOUT, null, false);
+
+ TextEdit replace = new ReplaceEdit(mSelectionStart, mSelectionEnd - mSelectionStart, xml);
+ rootEdit.addChild(replace);
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+ }
+
+ change.setEdit(rootEdit);
+ changes.add(change);
+ return changes;
+ }
+
+ @Nullable
+ private static String getPadding(@NonNull Element element, @NonNull String attribute) {
+ String padding = element.getAttributeNS(ANDROID_URI, attribute);
+ if (padding != null && padding.isEmpty()) {
+ padding = null;
+ }
+ return padding;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static String combine(@Nullable String dimension1, @Nullable String dimension2) {
+ if (dimension1 == null || dimension1.isEmpty()) {
+ if (dimension2 != null && dimension2.isEmpty()) {
+ return null;
+ }
+ return dimension2;
+ } else if (dimension2 == null || dimension2.isEmpty()) {
+ if (dimension1 != null && dimension1.isEmpty()) {
+ return null;
+ }
+ return dimension1;
+ } else {
+ // Two dimensions are specified (e.g. marginRight for the left one and marginLeft
+ // for the right one); we have to add these together. We can only do that if
+ // they use the same units, and do not use resources.
+ if (dimension1.startsWith(PREFIX_RESOURCE_REF)
+ || dimension2.startsWith(PREFIX_RESOURCE_REF)) {
+ return null;
+ }
+
+ Pattern p = Pattern.compile("([\\d\\.]+)(.+)"); //$NON-NLS-1$
+ Matcher matcher1 = p.matcher(dimension1);
+ Matcher matcher2 = p.matcher(dimension2);
+ if (matcher1.matches() && matcher2.matches()) {
+ String unit = matcher1.group(2);
+ if (unit.equals(matcher2.group(2))) {
+ float value1 = Float.parseFloat(matcher1.group(1));
+ float value2 = Float.parseFloat(matcher2.group(1));
+ return AdtUtils.formatFloatAttribute(value1 + value2) + unit;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets an Android attribute (in the Android namespace) on an element
+ * without a given namespace prefix. This is done when building a new Element
+ * in a temporary document such that the namespace prefix matches when the element is
+ * formatted and replaced in the target document.
+ */
+ private static void setAndroidAttribute(Element element, String prefix, String name,
+ String value) {
+ element.setAttribute(prefix + ':' + name, value);
+ }
+
+ @Override
+ public VisualRefactoringWizard createWizard() {
+ return new UseCompoundDrawableWizard(this, mDelegate);
+ }
+
+ @SuppressWarnings("javadoc")
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.usecompound", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new UseCompoundDrawableRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableWizard.java
new file mode 100644
index 000000000..3ffd6b5ea
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableWizard.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+class UseCompoundDrawableWizard extends VisualRefactoringWizard {
+ UseCompoundDrawableWizard(UseCompoundDrawableRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle("Use Compound Drawable");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ // This refactoring takes no parameters
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
new file mode 100644
index 000000000..904a3a084
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
@@ -0,0 +1,1403 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.XMLNS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.text.edits.DeleteEdit;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MalformedTreeException;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parent class for the various visual refactoring operations; contains shared
+ * implementations needed by most of them
+ */
+@SuppressWarnings("restriction") // XML model
+public abstract class VisualRefactoring extends Refactoring {
+ private static final String KEY_FILE = "file"; //$NON-NLS-1$
+ private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$
+ private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$
+ private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$
+
+ protected final IFile mFile;
+ protected final LayoutEditorDelegate mDelegate;
+ protected final IProject mProject;
+ protected int mSelectionStart = -1;
+ protected int mSelectionEnd = -1;
+ protected final List<Element> mElements;
+ protected final ITreeSelection mTreeSelection;
+ protected final ITextSelection mSelection;
+ /** Same as {@link #mSelectionStart} but not adjusted to element edges */
+ protected int mOriginalSelectionStart = -1;
+ /** Same as {@link #mSelectionEnd} but not adjusted to element edges */
+ protected int mOriginalSelectionEnd = -1;
+
+ protected final Map<Element, String> mGeneratedIdMap = new HashMap<Element, String>();
+ protected final Set<String> mGeneratedIds = new HashSet<String>();
+
+ protected List<Change> mChanges;
+ private String mAndroidNamespacePrefix;
+
+ /**
+ * This constructor is solely used by {@link VisualRefactoringDescriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ VisualRefactoring(Map<String, String> arguments) {
+ IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
+ mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ path = Path.fromPortableString(arguments.get(KEY_FILE));
+ mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
+ mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END));
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+ mDelegate = null;
+ mElements = null;
+ mSelection = null;
+ mTreeSelection = null;
+ }
+
+ @VisibleForTesting
+ VisualRefactoring(List<Element> elements, LayoutEditorDelegate delegate) {
+ mElements = elements;
+ mDelegate = delegate;
+
+ mFile = delegate != null ? delegate.getEditor().getInputFile() : null;
+ mProject = delegate != null ? delegate.getEditor().getProject() : null;
+ mSelectionStart = 0;
+ mSelectionEnd = 0;
+ mOriginalSelectionStart = 0;
+ mOriginalSelectionEnd = 0;
+ mSelection = null;
+ mTreeSelection = null;
+
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (Element element : elements) {
+ if (element instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) element;
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ mOriginalSelectionStart = start;
+ mOriginalSelectionEnd = end;
+ }
+ }
+
+ public VisualRefactoring(IFile file, LayoutEditorDelegate editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ mFile = file;
+ mDelegate = editor;
+ mProject = file.getProject();
+ mSelection = selection;
+ mTreeSelection = treeSelection;
+
+ // Initialize mSelectionStart and mSelectionEnd based on the selection context, which
+ // is either a treeSelection (when invoked from the layout editor or the outline), or
+ // a selection (when invoked from an XML editor)
+ if (treeSelection != null) {
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (TreePath path : treeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment;
+ UiViewElementNode uiNode = viewInfo.getUiViewNode();
+ if (uiNode == null) {
+ continue;
+ }
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xmlNode;
+
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+ }
+ if (selection != null) {
+ mOriginalSelectionStart = selection.getOffset();
+ mOriginalSelectionEnd = mOriginalSelectionStart + selection.getLength();
+ }
+ } else if (selection != null) {
+ // TODO: update selection to boundaries!
+ mSelectionStart = selection.getOffset();
+ mSelectionEnd = mSelectionStart + selection.getLength();
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+ }
+
+ mElements = initElements();
+ }
+
+ @NonNull
+ protected abstract List<Change> computeChanges(IProgressMonitor monitor);
+
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+ mChanges = new ArrayList<Change>();
+ try {
+ monitor.beginTask("Checking post-conditions...", 5);
+
+ // Reset state for each computeChanges call, in case the user goes back
+ // and forth in the refactoring wizard
+ mGeneratedIdMap.clear();
+ mGeneratedIds.clear();
+ List<Change> changes = computeChanges(monitor);
+ mChanges.addAll(changes);
+
+ monitor.worked(1);
+ } finally {
+ monitor.done();
+ }
+
+ return status;
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ try {
+ monitor.beginTask("Applying changes...", 1);
+
+ CompositeChange change = new CompositeChange(
+ getName(),
+ mChanges.toArray(new Change[mChanges.size()])) {
+ @Override
+ public ChangeDescriptor getDescriptor() {
+ VisualRefactoringDescriptor desc = createDescriptor();
+ return new RefactoringChangeDescriptor(desc);
+ }
+ };
+
+ monitor.worked(1);
+ return change;
+
+ } finally {
+ monitor.done();
+ }
+ }
+
+ protected abstract VisualRefactoringDescriptor createDescriptor();
+
+ protected Map<String, String> createArgumentMap() {
+ HashMap<String, String> args = new HashMap<String, String>();
+ args.put(KEY_PROJECT, mProject.getFullPath().toPortableString());
+ args.put(KEY_FILE, mFile.getFullPath().toPortableString());
+ args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
+ args.put(KEY_SEL_END, Integer.toString(mSelectionEnd));
+
+ return args;
+ }
+
+ IFile getFile() {
+ return mFile;
+ }
+
+ // ---- Shared functionality ----
+
+
+ protected void openFile(IFile file) {
+ GraphicalEditorPart graphicalEditor = mDelegate.getGraphicalEditor();
+ IFile leavingFile = graphicalEditor.getEditedFile();
+
+ try {
+ // Duplicate the current state into the newly created file
+ String state = ConfigurationDescription.getDescription(leavingFile);
+
+ // TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current
+ // theme to show.
+
+ file.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state);
+ } catch (CoreException e) {
+ // pass
+ }
+
+ /* TBD: "Show Included In" if supported.
+ * Not sure if this is a good idea.
+ if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ try {
+ Reference include = Reference.create(graphicalEditor.getEditedFile());
+ file.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, include);
+ } catch (CoreException e) {
+ // pass - worst that can happen is that we don't start with inclusion
+ }
+ }
+ */
+
+ try {
+ IEditorPart part =
+ IDE.openEditor(mDelegate.getEditor().getEditorSite().getPage(), file);
+ if (part instanceof AndroidXmlEditor && AdtPrefs.getPrefs().getFormatGuiXml()) {
+ AndroidXmlEditor newEditor = (AndroidXmlEditor) part;
+ newEditor.reformatDocument();
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open new included layout");
+ }
+ }
+
+
+ /** Produce a list of edits to replace references to the given id with the given new id */
+ protected static List<TextEdit> replaceIds(String androidNamePrefix,
+ IStructuredDocument doc, int skipStart, int skipEnd,
+ String rootId, String referenceId) {
+ if (rootId == null) {
+ return Collections.emptyList();
+ }
+
+ // We need to search for either @+id/ or @id/
+ String match1 = rootId;
+ String match2;
+ if (match1.startsWith(ID_PREFIX)) {
+ match2 = '"' + NEW_ID_PREFIX + match1.substring(ID_PREFIX.length()) + '"';
+ match1 = '"' + match1 + '"';
+ } else if (match1.startsWith(NEW_ID_PREFIX)) {
+ match2 = '"' + ID_PREFIX + match1.substring(NEW_ID_PREFIX.length()) + '"';
+ match1 = '"' + match1 + '"';
+ } else {
+ return Collections.emptyList();
+ }
+
+ String namePrefix = androidNamePrefix + ':' + ATTR_LAYOUT_RESOURCE_PREFIX;
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+
+ IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion();
+ for (; region != null; region = region.getNext()) {
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+
+ // Look at all attribute values and look for an id reference match
+ String attributeName = ""; //$NON-NLS-1$
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ attributeName = region.getText(subRegion);
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ // Only replace references in layout attributes
+ if (!attributeName.startsWith(namePrefix)) {
+ continue;
+ }
+ // Skip occurrences in the given skip range
+ int subRegionStart = regionStart + subRegion.getStart();
+ if (subRegionStart >= skipStart && subRegionStart <= skipEnd) {
+ continue;
+ }
+
+ String attributeValue = region.getText(subRegion);
+ if (attributeValue.equals(match1) || attributeValue.equals(match2)) {
+ int start = subRegionStart + 1; // skip quote
+ int end = start + rootId.length();
+
+ edits.add(new ReplaceEdit(start, end - start, referenceId));
+ }
+ }
+ }
+ }
+
+ return edits;
+ }
+
+ /** Get the id of the root selected element, if any */
+ protected String getRootId() {
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String oldId = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
+ // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
+ if (oldId != null && oldId.length() > 0) {
+ return oldId;
+ }
+ }
+
+ return null;
+ }
+
+ protected String getAndroidNamespacePrefix() {
+ if (mAndroidNamespacePrefix == null) {
+ List<Attr> attributeNodes = findNamespaceAttributes();
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ String name = attributeNode.getNodeName();
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ mAndroidNamespacePrefix = name;
+ if (mAndroidNamespacePrefix.startsWith(XMLNS_PREFIX)) {
+ mAndroidNamespacePrefix =
+ mAndroidNamespacePrefix.substring(XMLNS_PREFIX.length());
+ }
+ }
+ }
+ }
+
+ if (mAndroidNamespacePrefix == null) {
+ mAndroidNamespacePrefix = ANDROID_NS_NAME;
+ }
+ }
+
+ return mAndroidNamespacePrefix;
+ }
+
+ protected static String getAndroidNamespacePrefix(Document document) {
+ String nsPrefix = null;
+ List<Attr> attributeNodes = findNamespaceAttributes(document);
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ String name = attributeNode.getNodeName();
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ nsPrefix = name;
+ if (nsPrefix.startsWith(XMLNS_PREFIX)) {
+ nsPrefix =
+ nsPrefix.substring(XMLNS_PREFIX.length());
+ }
+ }
+ }
+ }
+
+ if (nsPrefix == null) {
+ nsPrefix = ANDROID_NS_NAME;
+ }
+
+ return nsPrefix;
+ }
+
+ protected List<Attr> findNamespaceAttributes() {
+ Document document = getDomDocument();
+ return findNamespaceAttributes(document);
+ }
+
+ protected static List<Attr> findNamespaceAttributes(Document document) {
+ if (document != null) {
+ Element root = document.getDocumentElement();
+ return findNamespaceAttributes(root);
+ }
+
+ return Collections.emptyList();
+ }
+
+ protected static List<Attr> findNamespaceAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ protected List<Attr> findLayoutAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String name = attributeNode.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ protected String insertNamespace(String xmlText, String namespaceDeclarations) {
+ // Insert namespace declarations into the extracted XML fragment
+ int firstSpace = xmlText.indexOf(' ');
+ int elementEnd = xmlText.indexOf('>');
+ int insertAt;
+ if (firstSpace != -1 && firstSpace < elementEnd) {
+ insertAt = firstSpace;
+ } else {
+ insertAt = elementEnd;
+ }
+ xmlText = xmlText.substring(0, insertAt) + namespaceDeclarations
+ + xmlText.substring(insertAt);
+
+ return xmlText;
+ }
+
+ /** Remove sections of the document that correspond to top level layout attributes;
+ * these are placed on the include element instead */
+ protected String stripTopLayoutAttributes(Element primary, int start, String xml) {
+ if (primary != null) {
+ // List of attributes to remove
+ List<IndexedRegion> skip = new ArrayList<IndexedRegion>();
+ NamedNodeMap attributes = primary.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attr = attributes.item(i);
+ String name = attr.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attr.getNamespaceURI())) {
+ if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // These are special and are left in
+ continue;
+ }
+
+ if (attr instanceof IndexedRegion) {
+ skip.add((IndexedRegion) attr);
+ }
+ }
+ }
+ if (skip.size() > 0) {
+ Collections.sort(skip, new Comparator<IndexedRegion>() {
+ // Sort in start order
+ @Override
+ public int compare(IndexedRegion r1, IndexedRegion r2) {
+ return r1.getStartOffset() - r2.getStartOffset();
+ }
+ });
+
+ // Successively cut out the various layout attributes
+ // TODO remove adjacent whitespace too (but not newlines, unless they
+ // are newly adjacent)
+ StringBuilder sb = new StringBuilder(xml.length());
+ int nextStart = 0;
+
+ // Copy out all the sections except the skip sections
+ for (IndexedRegion r : skip) {
+ int regionStart = r.getStartOffset();
+ // Adjust to string offsets since we've copied the string out of
+ // the document
+ regionStart -= start;
+
+ sb.append(xml.substring(nextStart, regionStart));
+
+ nextStart = regionStart + r.getLength();
+ }
+ if (nextStart < xml.length()) {
+ sb.append(xml.substring(nextStart));
+ }
+
+ return sb.toString();
+ }
+ }
+
+ return xml;
+ }
+
+ protected static String getIndent(String line, int max) {
+ int i = 0;
+ int n = Math.min(max, line.length());
+ for (; i < n; i++) {
+ char c = line.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ return line.substring(0, i);
+ }
+ }
+
+ if (n < line.length()) {
+ return line.substring(0, n);
+ } else {
+ return line;
+ }
+ }
+
+ protected static String dedent(String xml) {
+ String[] lines = xml.split("\n"); //$NON-NLS-1$
+ if (lines.length < 2) {
+ // The first line never has any indentation since we copy it out from the
+ // element start index
+ return xml;
+ }
+
+ String indentPrefix = getIndent(lines[1], lines[1].length());
+ for (int i = 2, n = lines.length; i < n; i++) {
+ String line = lines[i];
+
+ // Ignore blank lines
+ if (line.trim().length() == 0) {
+ continue;
+ }
+
+ indentPrefix = getIndent(line, indentPrefix.length());
+
+ if (indentPrefix.length() == 0) {
+ return xml;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String line : lines) {
+ if (line.startsWith(indentPrefix)) {
+ sb.append(line.substring(indentPrefix.length()));
+ } else {
+ sb.append(line);
+ }
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+
+ protected String getText(int start, int end) {
+ try {
+ IStructuredDocument document = mDelegate.getEditor().getStructuredDocument();
+ return document.get(start, end - start);
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ return null;
+ }
+ }
+
+ protected List<Element> getElements() {
+ return mElements;
+ }
+
+ protected List<Element> initElements() {
+ List<Element> nodes = new ArrayList<Element>();
+
+ assert mTreeSelection == null || mSelection == null :
+ "treeSel= " + mTreeSelection + ", sel=" + mSelection;
+
+ // Initialize mSelectionStart and mSelectionEnd based on the selection context, which
+ // is either a treeSelection (when invoked from the layout editor or the outline), or
+ // a selection (when invoked from an XML editor)
+ if (mTreeSelection != null) {
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (TreePath path : mTreeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment;
+ UiViewElementNode uiNode = viewInfo.getUiViewNode();
+ if (uiNode == null) {
+ continue;
+ }
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode instanceof Element) {
+ Element element = (Element) xmlNode;
+ nodes.add(element);
+ IndexedRegion region = getRegion(element);
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ }
+ } else if (mSelection != null) {
+ mSelectionStart = mSelection.getOffset();
+ mSelectionEnd = mSelectionStart + mSelection.getLength();
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+
+ // Figure out the range of selected nodes from the document offsets
+ IStructuredDocument doc = mDelegate.getEditor().getStructuredDocument();
+ Pair<Element, Element> range = DomUtilities.getElementRange(doc,
+ mSelectionStart, mSelectionEnd);
+ if (range != null) {
+ Element first = range.getFirst();
+ Element last = range.getSecond();
+
+ // Adjust offsets to get rid of surrounding text nodes (if you happened
+ // to select a text range and included whitespace on either end etc)
+ mSelectionStart = getRegion(first).getStartOffset();
+ mSelectionEnd = getRegion(last).getEndOffset();
+
+ if (mSelectionStart > mSelectionEnd) {
+ int tmp = mSelectionStart;
+ mSelectionStart = mSelectionEnd;
+ mSelectionEnd = tmp;
+ }
+
+ if (first == last) {
+ nodes.add(first);
+ } else if (first.getParentNode() == last.getParentNode()) {
+ // Add the range
+ Node node = first;
+ while (node != null) {
+ if (node instanceof Element) {
+ nodes.add((Element) node);
+ }
+ if (node == last) {
+ break;
+ }
+ node = node.getNextSibling();
+ }
+ } else {
+ // Different parents: this means we have an uneven selection, selecting
+ // elements from different levels. We can't extract ranges like that.
+ }
+ }
+ } else {
+ assert false;
+ }
+
+ // Make sure that the list of elements is unique
+ //Set<Element> seen = new HashSet<Element>();
+ //for (Element element : nodes) {
+ // assert !seen.contains(element) : element;
+ // seen.add(element);
+ //}
+
+ return nodes;
+ }
+
+ protected Element getPrimaryElement() {
+ List<Element> elements = getElements();
+ if (elements != null && elements.size() == 1) {
+ return elements.get(0);
+ }
+
+ return null;
+ }
+
+ protected Document getDomDocument() {
+ if (mDelegate.getUiRootNode() != null) {
+ return mDelegate.getUiRootNode().getXmlDocument();
+ } else {
+ return getElements().get(0).getOwnerDocument();
+ }
+ }
+
+ protected List<CanvasViewInfo> getSelectedViewInfos() {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mTreeSelection != null) {
+ for (TreePath path : mTreeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ infos.add((CanvasViewInfo) lastSegment);
+ }
+ }
+ }
+ return infos;
+ }
+
+ protected boolean validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ if (infos.size() == 0) {
+ status.addFatalError("No selection to extract");
+ return false;
+ }
+
+ return true;
+ }
+
+ protected boolean validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ for (CanvasViewInfo info : infos) {
+ if (info.isRoot()) {
+ status.addFatalError("Cannot refactor the root");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected boolean validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ if (infos.size() > 1) {
+ // All elements must be siblings (e.g. same parent)
+ List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
+ .size());
+ for (CanvasViewInfo info : infos) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ if (nodes.size() == 0) {
+ status.addFatalError("No selected views");
+ return false;
+ }
+
+ UiElementNode parent = nodes.get(0).getUiParent();
+ for (UiViewElementNode node : nodes) {
+ if (parent != node.getUiParent()) {
+ status.addFatalError("The selected elements must be adjacent");
+ return false;
+ }
+ }
+ // Ensure that the siblings are contiguous; no gaps.
+ // If we've selected all the children of the parent then we don't need
+ // to look.
+ List<UiElementNode> siblings = parent.getUiChildren();
+ if (siblings.size() != nodes.size()) {
+ Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
+ boolean inRange = false;
+ int remaining = nodes.size();
+ for (UiElementNode node : siblings) {
+ boolean in = nodeSet.contains(node);
+ if (in) {
+ remaining--;
+ if (remaining == 0) {
+ break;
+ }
+ inRange = true;
+ } else if (inRange) {
+ status.addFatalError("The selected elements must be adjacent");
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Updates the given element with a new name if the current id reflects the old
+ * element type. If the name was changed, it will return the new name.
+ */
+ protected String ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit) {
+ String oldType = element.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ String oldTypeBase = oldType.substring(oldType.lastIndexOf('.') + 1);
+ String id = getId(element);
+ if (id == null || id.length() == 0
+ || id.toLowerCase(Locale.US).contains(oldTypeBase.toLowerCase(Locale.US))) {
+ String newTypeBase = newType.substring(newType.lastIndexOf('.') + 1);
+ return ensureHasId(rootEdit, element, newTypeBase);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link IndexedRegion} for the given node
+ *
+ * @param node the node to look up the region for
+ * @return the corresponding region, or null
+ */
+ public static IndexedRegion getRegion(Node node) {
+ if (node instanceof IndexedRegion) {
+ return (IndexedRegion) node;
+ }
+
+ return null;
+ }
+
+ protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix) {
+ return ensureHasId(rootEdit, element, prefix, true);
+ }
+
+ protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix,
+ boolean apply) {
+ String id = mGeneratedIdMap.get(element);
+ if (id != null) {
+ return NEW_ID_PREFIX + id;
+ }
+
+ if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID)
+ || (prefix != null && !getId(element).startsWith(prefix))) {
+ id = DomUtilities.getFreeWidgetId(element, mGeneratedIds, prefix);
+ // Make sure we don't use this one again
+ mGeneratedIds.add(id);
+ mGeneratedIdMap.put(element, id);
+ id = NEW_ID_PREFIX + id;
+ if (apply) {
+ setAttribute(rootEdit, element,
+ ANDROID_URI, getAndroidNamespacePrefix(), ATTR_ID, id);
+ }
+ return id;
+ }
+
+ return getId(element);
+ }
+
+ protected int getFirstAttributeOffset(Element element) {
+ IndexedRegion region = getRegion(element);
+ if (region != null) {
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ String name = element.getLocalName();
+ int nameOffset = text.indexOf(name);
+ if (nameOffset != -1) {
+ return startOffset + nameOffset + name.length();
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the id of the given element
+ *
+ * @param element the element to look up the id for
+ * @return the corresponding id, or an empty string (should not be null
+ * according to the DOM API, but has been observed to be null on
+ * some versions of Eclipse)
+ */
+ public static String getId(Element element) {
+ return element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ }
+
+ protected String ensureNewId(String id) {
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ } else if (!id.startsWith(NEW_ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id;
+ }
+ } else {
+ id = null;
+ }
+
+ return id;
+ }
+
+ protected String getViewClass(String fqcn) {
+ // Don't include android.widget. as a package prefix in layout files
+ if (fqcn.startsWith(ANDROID_WIDGET_PREFIX)) {
+ fqcn = fqcn.substring(ANDROID_WIDGET_PREFIX.length());
+ }
+
+ return fqcn;
+ }
+
+ protected void setAttribute(MultiTextEdit rootEdit, Element element,
+ String attributeUri,
+ String attributePrefix, String attributeName, String attributeValue) {
+ int offset = getFirstAttributeOffset(element);
+ if (offset != -1) {
+ if (element.hasAttributeNS(attributeUri, attributeName)) {
+ replaceAttributeDeclaration(rootEdit, offset, element, attributePrefix,
+ attributeUri, attributeName, attributeValue);
+ } else {
+ addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName,
+ attributeValue);
+ }
+ }
+ }
+
+ private void addAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ String attributePrefix, String attributeName, String attributeValue) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(' ');
+
+ if (attributePrefix != null) {
+ sb.append(attributePrefix).append(':');
+ }
+ sb.append(attributeName).append('=').append('"');
+ sb.append(attributeValue).append('"');
+
+ InsertEdit setAttribute = new InsertEdit(offset, sb.toString());
+ rootEdit.addChild(setAttribute);
+ }
+
+ /** Replaces the value declaration of the given attribute */
+ private void replaceAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ Element element, String attributePrefix, String attributeUri,
+ String attributeName, String attributeValue) {
+ // Find attribute value and replace it
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+
+ int valueStart = -1;
+ boolean useNextValue = false;
+ String targetName = attributePrefix != null
+ ? attributePrefix + ':' + attributeName : attributeName;
+
+ // Look at all attribute values and look for an id reference match
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ // What about prefix?
+ if (targetName.equals(region.getText(subRegion))) {
+ useNextValue = true;
+ }
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ if (useNextValue) {
+ valueStart = regionStart + subRegion.getStart();
+ break;
+ }
+ }
+ }
+
+ if (valueStart != -1) {
+ String oldValue = element.getAttributeNS(attributeUri, attributeName);
+ int start = valueStart + 1; // Skip opening "
+ ReplaceEdit setAttribute = new ReplaceEdit(start, oldValue.length(),
+ attributeValue);
+ try {
+ rootEdit.addChild(setAttribute);
+ } catch (MalformedTreeException mte) {
+ AdtPlugin.log(mte, "Could not replace attribute %1$s with %2$s",
+ attributeName, attributeValue);
+ throw mte;
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ /** Strips out the given attribute, if defined */
+ protected void removeAttribute(MultiTextEdit rootEdit, Element element, String uri,
+ String attributeName) {
+ if (element.hasAttributeNS(uri, attributeName)) {
+ Attr attribute = element.getAttributeNodeNS(uri, attributeName);
+ removeAttribute(rootEdit, attribute);
+ }
+ }
+
+ /** Strips out the given attribute, if defined */
+ protected void removeAttribute(MultiTextEdit rootEdit, Attr attribute) {
+ IndexedRegion region = getRegion(attribute);
+ if (region != null) {
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ rootEdit.addChild(deletion);
+ }
+ }
+
+
+ /**
+ * Removes the given element's opening and closing tags (including all of its
+ * attributes) but leaves any children alone
+ *
+ * @param rootEdit the multi edit to add the removal operation to
+ * @param element the element to delete the open and closing tags for
+ * @param skip a list of elements that should not be modified (for example because they
+ * are targeted for deletion)
+ *
+ * TODO: Rename this to "unwrap" ? And allow for handling nested deletions.
+ */
+ protected void removeElementTags(MultiTextEdit rootEdit, Element element, List<Element> skip,
+ boolean changeIndentation) {
+ IndexedRegion elementRegion = getRegion(element);
+ if (elementRegion == null) {
+ return;
+ }
+
+ // Look for the opening tag
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ int startLineInclusive = -1;
+ int endLineInclusive = -1;
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ int start = elementRegion.getStartOffset();
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+ int startOffset = regionStart;
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_OPEN.equals(type)) {
+ startOffset = regionStart + subRegion.getStart();
+ } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) {
+ int endOffset = regionStart + subRegion.getStart() + subRegion.getLength();
+
+ DeleteEdit deletion = createDeletion(doc, startOffset, endOffset);
+ rootEdit.addChild(deletion);
+ startLineInclusive = doc.getLineOfOffset(endOffset) + 1;
+ break;
+ }
+ }
+
+ // Find the close tag
+ // Look at all attribute values and look for an id reference match
+ region = doc.getRegionAtCharacterOffset(elementRegion.getEndOffset()
+ - element.getTagName().length() - 1);
+ list = region.getRegions();
+ regionStart = region.getStartOffset();
+ startOffset = -1;
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
+ startOffset = regionStart + subRegion.getStart();
+ } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) {
+ int endOffset = regionStart + subRegion.getStart() + subRegion.getLength();
+ if (startOffset != -1) {
+ DeleteEdit deletion = createDeletion(doc, startOffset, endOffset);
+ rootEdit.addChild(deletion);
+ endLineInclusive = doc.getLineOfOffset(startOffset) - 1;
+ }
+ break;
+ }
+ }
+ }
+
+ // Dedent the contents
+ if (changeIndentation && startLineInclusive != -1 && endLineInclusive != -1) {
+ String indent = AndroidXmlEditor.getIndentAtOffset(doc, getRegion(element)
+ .getStartOffset());
+ setIndentation(rootEdit, indent, doc, startLineInclusive, endLineInclusive,
+ element, skip);
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ protected void removeIndentation(MultiTextEdit rootEdit, String removeIndent,
+ IStructuredDocument doc, int startLineInclusive, int endLineInclusive,
+ Element element, List<Element> skip) {
+ if (startLineInclusive > endLineInclusive) {
+ return;
+ }
+ int indentLength = removeIndent.length();
+ if (indentLength == 0) {
+ return;
+ }
+
+ try {
+ for (int line = startLineInclusive; line <= endLineInclusive; line++) {
+ IRegion info = doc.getLineInformation(line);
+ int lineStart = info.getOffset();
+ int lineLength = info.getLength();
+ int lineEnd = lineStart + lineLength;
+ if (overlaps(lineStart, lineEnd, element, skip)) {
+ continue;
+ }
+ String lineText = getText(lineStart,
+ lineStart + Math.min(lineLength, indentLength));
+ if (lineText.startsWith(removeIndent)) {
+ rootEdit.addChild(new DeleteEdit(lineStart, indentLength));
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ protected void setIndentation(MultiTextEdit rootEdit, String indent,
+ IStructuredDocument doc, int startLineInclusive, int endLineInclusive,
+ Element element, List<Element> skip) {
+ if (startLineInclusive > endLineInclusive) {
+ return;
+ }
+ int indentLength = indent.length();
+ if (indentLength == 0) {
+ return;
+ }
+
+ try {
+ for (int line = startLineInclusive; line <= endLineInclusive; line++) {
+ IRegion info = doc.getLineInformation(line);
+ int lineStart = info.getOffset();
+ int lineLength = info.getLength();
+ int lineEnd = lineStart + lineLength;
+ if (overlaps(lineStart, lineEnd, element, skip)) {
+ continue;
+ }
+ String lineText = getText(lineStart, lineStart + lineLength);
+ int indentEnd = getFirstNonSpace(lineText);
+ rootEdit.addChild(new ReplaceEdit(lineStart, indentEnd, indent));
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private int getFirstNonSpace(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return i;
+ }
+ }
+
+ return s.length();
+ }
+
+ /** Returns true if the given line overlaps any of the given elements */
+ private static boolean overlaps(int startOffset, int endOffset,
+ Element element, List<Element> overlaps) {
+ for (Element e : overlaps) {
+ if (e == element) {
+ continue;
+ }
+
+ IndexedRegion region = getRegion(e);
+ if (region.getEndOffset() >= startOffset && region.getStartOffset() <= endOffset) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected DeleteEdit createDeletion(IStructuredDocument doc, int startOffset, int endOffset) {
+ // Expand to delete the whole line?
+ try {
+ IRegion info = doc.getLineInformationOfOffset(startOffset);
+ int lineBegin = info.getOffset();
+ // Is the text on the line leading up to the deletion region,
+ // and the text following it, all whitespace?
+ boolean deleteLine = true;
+ if (lineBegin < startOffset) {
+ String prefix = getText(lineBegin, startOffset);
+ if (prefix.trim().length() > 0) {
+ deleteLine = false;
+ }
+ }
+ info = doc.getLineInformationOfOffset(endOffset);
+ int lineEnd = info.getOffset() + info.getLength();
+ if (lineEnd > endOffset) {
+ String suffix = getText(endOffset, lineEnd);
+ if (suffix.trim().length() > 0) {
+ deleteLine = false;
+ }
+ }
+ if (deleteLine) {
+ startOffset = lineBegin;
+ endOffset = Math.min(doc.getLength(), lineEnd + 1);
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+
+ return new DeleteEdit(startOffset, endOffset - startOffset);
+ }
+
+ /**
+ * Rewrite the edits in the given {@link MultiTextEdit} such that same edits are
+ * applied, but the resulting range is also formatted
+ */
+ protected MultiTextEdit reformat(MultiTextEdit edit, XmlFormatStyle style) {
+ String xml = mDelegate.getEditor().getStructuredDocument().get();
+ return reformat(xml, edit, style);
+ }
+
+ /**
+ * Rewrite the edits in the given {@link MultiTextEdit} such that same edits are
+ * applied, but the resulting range is also formatted
+ *
+ * @param oldContents the original contents that should be edited by a
+ * {@link MultiTextEdit}
+ * @param edit the {@link MultiTextEdit} to be applied to some string
+ * @param style the formatting style to use
+ * @return a new {@link MultiTextEdit} which performs the same edits as the input edit
+ * but also reformats the text
+ */
+ public static MultiTextEdit reformat(String oldContents, MultiTextEdit edit,
+ XmlFormatStyle style) {
+ IDocument document = new org.eclipse.jface.text.Document();
+ document.set(oldContents);
+
+ try {
+ edit.apply(document);
+ } catch (MalformedTreeException e) {
+ AdtPlugin.log(e, null);
+ return null; // Abort formatting
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ return null; // Abort formatting
+ }
+
+ String actual = document.get();
+
+ // TODO: Try to format only the affected portion of the document.
+ // To do that we need to find out what the affected offsets are; we know
+ // the MultiTextEdit's affected range, but that is referring to offsets
+ // in the old document. Use that to compute offsets in the new document.
+ //int distanceFromEnd = actual.length() - edit.getExclusiveEnd();
+ //IStructuredModel model = DomUtilities.createStructuredModel(actual);
+ //int start = edit.getOffset();
+ //int end = actual.length() - distanceFromEnd;
+ //int length = end - start;
+ //TextEdit format = AndroidXmlFormattingStrategy.format(model, start, length);
+ EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create();
+ String formatted = EclipseXmlPrettyPrinter.prettyPrint(actual, formatPrefs, style,
+ null /*lineSeparator*/);
+
+
+ // Figure out how much of the before and after strings are identical and narrow
+ // the replacement scope
+ boolean foundDifference = false;
+ int firstDifference = 0;
+ int lastDifference = formatted.length();
+ int start = 0;
+ int end = oldContents.length();
+
+ for (int i = 0, j = start; i < formatted.length() && j < end; i++, j++) {
+ if (formatted.charAt(i) != oldContents.charAt(j)) {
+ firstDifference = i;
+ foundDifference = true;
+ break;
+ }
+ }
+
+ if (!foundDifference) {
+ // No differences - the document is already formatted, nothing to do
+ return null;
+ }
+
+ lastDifference = firstDifference + 1;
+ for (int i = formatted.length() - 1, j = end - 1;
+ i > firstDifference && j > start;
+ i--, j--) {
+ if (formatted.charAt(i) != oldContents.charAt(j)) {
+ lastDifference = i + 1;
+ break;
+ }
+ }
+
+ start += firstDifference;
+ end -= (formatted.length() - lastDifference);
+ end = Math.max(start, end);
+ formatted = formatted.substring(firstDifference, lastDifference);
+
+ ReplaceEdit format = new ReplaceEdit(start, end - start,
+ formatted);
+
+ MultiTextEdit newEdit = new MultiTextEdit();
+ newEdit.addChild(format);
+
+ return newEdit;
+ }
+
+ protected ViewElementDescriptor getElementDescriptor(String fqcn) {
+ AndroidTargetData data = mDelegate.getEditor().getTargetData();
+ if (data != null) {
+ return data.getLayoutDescriptors().findDescriptorByClass(fqcn);
+ }
+
+ return null;
+ }
+
+ /** Create a wizard for this refactoring */
+ abstract VisualRefactoringWizard createWizard();
+
+ public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor {
+ private final Map<String, String> mArguments;
+
+ public VisualRefactoringDescriptor(
+ String id, String project, String description, String comment,
+ Map<String, String> arguments) {
+ super(id, project, description, comment, STRUCTURAL_CHANGE | MULTI_CHANGE);
+ mArguments = arguments;
+ }
+
+ public Map<String, String> getArguments() {
+ return mArguments;
+ }
+
+ protected abstract Refactoring createRefactoring(Map<String, String> args);
+
+ @Override
+ public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+ try {
+ return createRefactoring(mArguments);
+ } catch (NullPointerException e) {
+ status.addFatalError("Failed to recreate refactoring from descriptor");
+ return null;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java
new file mode 100644
index 000000000..f1cc988d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.part.FileEditorInput;
+
+abstract class VisualRefactoringAction implements IWorkbenchWindowActionDelegate {
+ protected IWorkbenchWindow mWindow;
+ protected ITextSelection mTextSelection;
+ protected ITreeSelection mTreeSelection;
+ protected LayoutEditorDelegate mDelegate;
+ protected IFile mFile;
+
+ /**
+ * Keep track of the current workbench window.
+ */
+ @Override
+ public void init(IWorkbenchWindow window) {
+ mWindow = window;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ /**
+ * Examine the selection to determine if the action should be enabled or not.
+ * <p/>
+ * Keep a link to the relevant selection structure
+ */
+ @Override
+ public void selectionChanged(IAction action, ISelection selection) {
+ // Look for selections in XML and in the layout UI editor
+
+ // Note, two kinds of selections are returned here:
+ // ITextSelection on a Java source window
+ // IStructuredSelection in the outline or navigator
+ // This simply deals with the refactoring based on a non-empty selection.
+ // At that point, just enable the action and later decide if it's valid when it actually
+ // runs since we don't have access to the AST yet.
+
+ mTextSelection = null;
+ mTreeSelection = null;
+ mFile = null;
+
+ IEditorPart editor = null;
+
+ if (selection instanceof ITextSelection) {
+ mTextSelection = (ITextSelection) selection;
+ editor = AdtUtils.getActiveEditor();
+ mFile = getSelectedFile(editor);
+ } else if (selection instanceof ITreeSelection) {
+ Object firstElement = ((ITreeSelection)selection).getFirstElement();
+ if (firstElement instanceof CanvasViewInfo) {
+ mTreeSelection = (ITreeSelection) selection;
+ editor = AdtUtils.getActiveEditor();
+ mFile = getSelectedFile(editor);
+ }
+ }
+
+ mDelegate = LayoutEditorDelegate.fromEditor(editor);
+
+ action.setEnabled((mTextSelection != null || mTreeSelection != null)
+ && mFile != null && mDelegate != null);
+ }
+
+ /**
+ * Create a new instance of our refactoring and a wizard to configure it.
+ */
+ @Override
+ public abstract void run(IAction action);
+
+ /**
+ * Returns the active {@link IFile} (hopefully matching our selection) or null.
+ * The file is only returned if it's a file from a project with an Android nature.
+ * <p/>
+ * At that point we do not try to analyze if the selection nor the file is suitable
+ * for the refactoring. This check is performed when the refactoring is invoked since
+ * it can then produce meaningful error messages as needed.
+ */
+ private IFile getSelectedFile(IEditorPart editor) {
+ if (editor != null) {
+ IEditorInput input = editor.getEditorInput();
+
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fi = (FileEditorInput) input;
+ IFile file = fi.getFile();
+ if (file.exists()) {
+ IProject proj = file.getProject();
+ try {
+ if (proj != null && proj.hasNature(AdtConstants.NATURE_DEFAULT)) {
+ return file;
+ }
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static IAction create(String title, LayoutEditorDelegate editorDelegate,
+ Class<? extends VisualRefactoringAction> clz) {
+ return new ActionWrapper(title, editorDelegate, clz);
+ }
+
+ private static class ActionWrapper extends Action {
+ private Class<? extends VisualRefactoringAction> mClass;
+ private LayoutEditorDelegate mEditorDelegate;
+
+ ActionWrapper(String title, LayoutEditorDelegate editorDelegate,
+ Class<? extends VisualRefactoringAction> clz) {
+ super(title);
+ mEditorDelegate = editorDelegate;
+ mClass = clz;
+ }
+
+ @Override
+ public void run() {
+ VisualRefactoringAction action;
+ try {
+ action = mClass.newInstance();
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ return;
+ }
+ IEditorSite site = mEditorDelegate.getEditor().getEditorSite();
+ action.init(site.getWorkbenchWindow());
+ ISelection selection = site.getSelectionProvider().getSelection();
+ action.selectionChanged(ActionWrapper.this, selection);
+ if (isEnabled()) {
+ action.run(ActionWrapper.this);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java
new file mode 100644
index 000000000..c103e47dc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+
+public abstract class VisualRefactoringWizard extends RefactoringWizard {
+ protected final LayoutEditorDelegate mDelegate;
+
+ public VisualRefactoringWizard(Refactoring refactoring, LayoutEditorDelegate editor) {
+ super(refactoring, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+ mDelegate = editor;
+ }
+
+ @Override
+ public boolean performFinish() {
+ mDelegate.getEditor().setIgnoreXmlUpdate(true);
+ try {
+ return super.performFinish();
+ } finally {
+ mDelegate.getEditor().setIgnoreXmlUpdate(false);
+ mDelegate.refreshXmlModel();
+ }
+ }
+
+ protected abstract static class VisualRefactoringInputPage extends UserInputWizardPage {
+ public VisualRefactoringInputPage(String name) {
+ super(name);
+ }
+
+ /**
+ * Listener which can be attached on any widget in the wizard page to force
+ * modifications of the associated widget to validate the page again
+ */
+ protected ModifyListener mModifyValidateListener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+ };
+
+ /**
+ * Listener which can be attached on any widget in the wizard page to force
+ * selection changes of the associated widget to validate the page again
+ */
+ protected SelectionAdapter mSelectionValidateListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ }
+ };
+
+ protected abstract boolean validatePage();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java
new file mode 100644
index 000000000..1cd66596b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Wrap In" menu item is invoked.
+ */
+public class WrapInAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ WrapInRefactoring ref = new WrapInRefactoring(mFile, mDelegate,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new WrapInWizard(ref, mDelegate);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditorDelegate editorDelegate) {
+ return create("Wrap in Container...", editorDelegate, WrapInAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java
new file mode 100644
index 000000000..61d7987d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class WrapInContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new WrapInRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof WrapInRefactoring.Descriptor) {
+ return ((WrapInRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
new file mode 100644
index 000000000..07b00b8da
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.DeleteEdit;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Inserts a new layout surrounding the current selection, migrates namespace
+ * attributes (if wrapping the root node), and optionally migrates layout
+ * attributes and updates references elsewhere.
+ */
+@SuppressWarnings("restriction") // XML model
+public class WrapInRefactoring extends VisualRefactoring {
+ private static final String KEY_ID = "name"; //$NON-NLS-1$
+ private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+
+ private String mId;
+ private String mTypeFqcn;
+ private String mInitializedAttributes;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ WrapInRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mId = arguments.get(KEY_ID);
+ mTypeFqcn = arguments.get(KEY_TYPE);
+ }
+
+ public WrapInRefactoring(
+ IFile file,
+ LayoutEditorDelegate delegate,
+ ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, delegate, selection, treeSelection);
+ }
+
+ @VisibleForTesting
+ WrapInRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
+ super(selectedElements, editor);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to wrap");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ // TODO - don't do this if we based the selection on text. In this case,
+ // make sure we're -balanced-.
+
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+
+ // Enforce that the selection is -contiguous-
+ if (!validateContiguous(infos, status)) {
+ return status;
+ }
+ }
+
+ // Ensures that we have a valid DOM model:
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to wrap");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_TYPE, mTypeFqcn);
+ args.put(KEY_ID, mId);
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Wrap in Container";
+ }
+
+ void setId(String id) {
+ mId = id;
+ }
+
+ void setType(String typeFqcn) {
+ mTypeFqcn = typeFqcn;
+ }
+
+ void setInitializedAttributes(String initializedAttributes) {
+ mInitializedAttributes = initializedAttributes;
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ // (1) Insert the new container in front of the beginning of the
+ // first wrapped view
+ // (2) If the container is the new root, transfer namespace declarations
+ // to it
+ // (3) Insert the closing tag of the new container at the end of the
+ // last wrapped view
+ // (4) Reindent the wrapped views
+ // (5) If the user requested it, update all layout references to the
+ // wrapped views with the new container?
+ // For that matter, does RelativeLayout even require it? Probably not,
+ // it can point inside the current layout...
+
+ // Add indent to all lines between mSelectionStart and mEnd
+ // TODO: Figure out the indentation amount?
+ // For now, use 4 spaces
+ String indentUnit = " "; //$NON-NLS-1$
+ boolean separateAttributes = true;
+ IStructuredDocument document = mDelegate.getEditor().getStructuredDocument();
+ String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart);
+
+ String viewClass = getViewClass(mTypeFqcn);
+ String androidNsPrefix = getAndroidNamespacePrefix();
+
+
+ IFile file = mDelegate.getEditor().getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ if (file == null) {
+ return changes;
+ }
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setTextType(EXT_XML);
+
+ String id = ensureNewId(mId);
+
+ // Update any layout references to the old id with the new id
+ if (id != null) {
+ String rootId = getRootId();
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
+ doc, mSelectionStart, mSelectionEnd, rootId, id);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ // Insert namespace elements?
+ StringBuilder namespace = null;
+ List<DeleteEdit> deletions = new ArrayList<DeleteEdit>();
+ Element primary = getPrimaryElement();
+ if (primary != null && getDomDocument().getDocumentElement() == primary) {
+ namespace = new StringBuilder();
+
+ List<Attr> declarations = findNamespaceAttributes(primary);
+ for (Attr attribute : declarations) {
+ if (attribute instanceof IndexedRegion) {
+ // Delete the namespace declaration in the node which is no longer the root
+ IndexedRegion region = (IndexedRegion) attribute;
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ deletions.add(deletion);
+ rootEdit.addChild(deletion);
+ text = text.trim();
+
+ // Insert the namespace declaration in the new root
+ if (separateAttributes) {
+ namespace.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ namespace.append(' ');
+ }
+ namespace.append(text);
+ }
+ }
+ }
+
+ // Insert begin tag: <type ...>
+ StringBuilder sb = new StringBuilder();
+ sb.append('<');
+ sb.append(viewClass);
+
+ if (namespace != null) {
+ sb.append(namespace);
+ }
+
+ // Set the ID if any
+ if (id != null) {
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(androidNsPrefix).append(':');
+ sb.append(ATTR_ID).append('=').append('"').append(id).append('"');
+ }
+
+ // If any of the elements are fill/match parent, use that instead
+ String width = VALUE_WRAP_CONTENT;
+ String height = VALUE_WRAP_CONTENT;
+
+ for (Element element : getElements()) {
+ String oldWidth = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ String oldHeight = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+
+ if (VALUE_MATCH_PARENT.equals(oldWidth) || VALUE_FILL_PARENT.equals(oldWidth)) {
+ width = oldWidth;
+ }
+ if (VALUE_MATCH_PARENT.equals(oldHeight) || VALUE_FILL_PARENT.equals(oldHeight)) {
+ height = oldHeight;
+ }
+ }
+
+ // Add in width/height.
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(androidNsPrefix).append(':');
+ sb.append(ATTR_LAYOUT_WIDTH).append('=').append('"').append(width).append('"');
+
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(androidNsPrefix).append(':');
+ sb.append(ATTR_LAYOUT_HEIGHT).append('=').append('"').append(height).append('"');
+
+ if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) {
+ for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$
+ sb.append(' ');
+ String[] nameValue = s.split("="); //$NON-NLS-1$
+ String name = nameValue[0];
+ String value = nameValue[1];
+ if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
+ name = name.substring(ANDROID_NS_NAME_PREFIX.length());
+ sb.append(androidNsPrefix).append(':');
+ }
+ sb.append(name).append('=').append('"').append(value).append('"');
+ }
+ }
+
+ // Transfer layout_ attributes (other than width and height)
+ if (primary != null) {
+ List<Attr> layoutAttributes = findLayoutAttributes(primary);
+ for (Attr attribute : layoutAttributes) {
+ String name = attribute.getLocalName();
+ if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // Already handled specially
+ continue;
+ }
+
+ if (attribute instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) attribute;
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ rootEdit.addChild(deletion);
+ deletions.add(deletion);
+
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(text.trim());
+ }
+ }
+ }
+
+ // Finish open tag:
+ sb.append('>');
+ sb.append('\n').append(startIndent).append(indentUnit);
+
+ InsertEdit beginEdit = new InsertEdit(mSelectionStart, sb.toString());
+ rootEdit.addChild(beginEdit);
+
+ String nested = getText(mSelectionStart, mSelectionEnd);
+ int index = 0;
+ while (index != -1) {
+ index = nested.indexOf('\n', index);
+ if (index != -1) {
+ index++;
+ InsertEdit newline = new InsertEdit(mSelectionStart + index, indentUnit);
+ // Some of the deleted namespaces may have had newlines - be careful
+ // not to overlap edits
+ boolean covered = false;
+ for (DeleteEdit deletion : deletions) {
+ if (deletion.covers(newline)) {
+ covered = true;
+ break;
+ }
+ }
+ if (!covered) {
+ rootEdit.addChild(newline);
+ }
+ }
+ }
+
+ // Insert end tag: </type>
+ sb.setLength(0);
+ sb.append('\n').append(startIndent);
+ sb.append('<').append('/').append(viewClass).append('>');
+ InsertEdit endEdit = new InsertEdit(mSelectionEnd, sb.toString());
+ rootEdit.addChild(endEdit);
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+ }
+
+ change.setEdit(rootEdit);
+ changes.add(change);
+ return changes;
+ }
+
+ String getOldType() {
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String oldType = primary.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ return oldType;
+ }
+
+ return null;
+ }
+
+ @Override
+ VisualRefactoringWizard createWizard() {
+ return new WrapInWizard(this, mDelegate);
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.wrapin", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new WrapInRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java
new file mode 100644
index 000000000..2e06a3bbd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.FQCN_RADIO_BUTTON;
+import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.RADIO_GROUP;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CustomViewFinder;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class WrapInWizard extends VisualRefactoringWizard {
+ private static final String SEPARATOR_LABEL =
+ "----------------------------------------"; //$NON-NLS-1$
+
+ public WrapInWizard(WrapInRefactoring ref, LayoutEditorDelegate editor) {
+ super(ref, editor);
+ setDefaultPageTitle("Wrap in Container");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ WrapInRefactoring ref = (WrapInRefactoring) getRefactoring();
+ String oldType = ref.getOldType();
+ addPage(new InputPage(mDelegate.getEditor().getProject(), oldType));
+ }
+
+ /** Wizard page which inputs parameters for the {@link WrapInRefactoring} operation */
+ private static class InputPage extends VisualRefactoringInputPage {
+ private final IProject mProject;
+ private final String mOldType;
+ private Text mIdText;
+ private Combo mTypeCombo;
+ private List<Pair<String, ViewElementDescriptor>> mClassNames;
+
+ public InputPage(IProject project, String oldType) {
+ super("WrapInInputPage"); //$NON-NLS-1$
+ mProject = project;
+ mOldType = oldType;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label typeLabel = new Label(composite, SWT.NONE);
+ typeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ typeLabel.setText("Type of Container:");
+
+ mTypeCombo = new Combo(composite, SWT.READ_ONLY);
+ mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mTypeCombo.addSelectionListener(mSelectionValidateListener);
+
+ Label idLabel = new Label(composite, SWT.NONE);
+ idLabel.setText("New Layout Id:");
+ idLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+
+ mIdText = new Text(composite, SWT.BORDER);
+ mIdText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mIdText.addModifyListener(mModifyValidateListener);
+
+ Set<String> exclude = Collections.singleton(VIEW_INCLUDE);
+ mClassNames = addLayouts(mProject, mOldType, mTypeCombo, exclude, true);
+ mTypeCombo.select(0);
+
+ setControl(composite);
+ validatePage();
+
+ mTypeCombo.setFocus();
+ }
+
+ @Override
+ protected boolean validatePage() {
+ boolean ok = true;
+
+ String id = mIdText.getText().trim();
+
+ if (id.length() == 0) {
+ setErrorMessage("ID required");
+ ok = false;
+ } else {
+ // ...but if you do, it has to be valid!
+ ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
+ ResourceType.ID);
+ String message = validator.isValid(id);
+ if (message != null) {
+ setErrorMessage(message);
+ ok = false;
+ }
+ }
+
+ int selectionIndex = mTypeCombo.getSelectionIndex();
+ String type = selectionIndex != -1 ? mClassNames.get(selectionIndex).getFirst() : null;
+ if (type == null) {
+ setErrorMessage("Select a container type");
+ ok = false; // The user has chosen a separator
+ }
+
+ if (ok) {
+ setErrorMessage(null);
+
+ // Record state
+ WrapInRefactoring refactoring =
+ (WrapInRefactoring) getRefactoring();
+ refactoring.setId(id);
+ refactoring.setType(type);
+
+ ViewElementDescriptor descriptor = mClassNames.get(selectionIndex).getSecond();
+ if (descriptor instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor paletteDescriptor =
+ (PaletteMetadataDescriptor) descriptor;
+ String initializedAttributes = paletteDescriptor.getInitializedAttributes();
+ refactoring.setInitializedAttributes(initializedAttributes);
+ } else {
+ refactoring.setInitializedAttributes(null);
+ }
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+
+ static List<Pair<String, ViewElementDescriptor>> addLayouts(IProject project,
+ String oldType, Combo combo,
+ Set<String> exclude, boolean addGestureOverlay) {
+ List<Pair<String, ViewElementDescriptor>> classNames =
+ new ArrayList<Pair<String, ViewElementDescriptor>>();
+
+ if (oldType != null && oldType.equals(FQCN_RADIO_BUTTON)) {
+ combo.add(RADIO_GROUP);
+ // NOT a fully qualified name since android widgets do not include the package
+ classNames.add(Pair.of(RADIO_GROUP, (ViewElementDescriptor) null));
+
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
+ }
+
+ Pair<List<String>,List<String>> result = CustomViewFinder.findViews(project, true);
+ List<String> customViews = result.getFirst();
+ List<String> thirdPartyViews = result.getSecond();
+ if (customViews.size() > 0) {
+ for (String view : customViews) {
+ combo.add(view);
+ classNames.add(Pair.of(view, (ViewElementDescriptor) null));
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
+ }
+
+ // Populate type combo
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData targetData = currentSdk.getTargetData(target);
+ if (targetData != null) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ List<Pair<String,List<ViewElementDescriptor>>> entries =
+ repository.getPaletteEntries(targetData, false, true);
+ // Find the layout category - it contains LinearLayout
+ List<ViewElementDescriptor> layoutDescriptors = null;
+
+ search: for (Pair<String,List<ViewElementDescriptor>> pair : entries) {
+ List<ViewElementDescriptor> list = pair.getSecond();
+ for (ViewElementDescriptor d : list) {
+ if (d.getFullClassName().equals(FQCN_LINEAR_LAYOUT)) {
+ // Found - use this list
+ layoutDescriptors = list;
+ break search;
+ }
+ }
+ }
+ if (layoutDescriptors != null) {
+ for (ViewElementDescriptor d : layoutDescriptors) {
+ String className = d.getFullClassName();
+ if (exclude == null || !exclude.contains(className)) {
+ combo.add(d.getUiName());
+ classNames.add(Pair.of(className, d));
+ }
+ }
+
+ // SWT does not support separators in combo boxes
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+
+ if (thirdPartyViews.size() > 0) {
+ for (String view : thirdPartyViews) {
+ combo.add(view);
+ classNames.add(Pair.of(view, (ViewElementDescriptor) null));
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ if (addGestureOverlay) {
+ combo.add(GESTURE_OVERLAY_VIEW);
+ classNames.add(Pair.<String, ViewElementDescriptor> of(
+ FQCN_GESTURE_OVERLAY_VIEW, null));
+
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
+ }
+ }
+
+ // Now add ALL known layout descriptors in case the user has
+ // a special case
+ layoutDescriptors =
+ targetData.getLayoutDescriptors().getLayoutDescriptors();
+
+ for (ViewElementDescriptor d : layoutDescriptors) {
+ String className = d.getFullClassName();
+ if (exclude == null || !exclude.contains(className)) {
+ combo.add(d.getUiName());
+ classNames.add(Pair.of(className, d));
+ }
+ }
+ }
+ }
+ } else {
+ combo.add("SDK not initialized");
+ classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
+ }
+
+ return classNames;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java
new file mode 100644
index 000000000..d9d272224
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.uimodel;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.FQCN_FRAME_LAYOUT;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.graphics.Image;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Specialized version of {@link UiElementNode} for the {@link ViewElementDescriptor}s.
+ */
+public class UiViewElementNode extends UiElementNode {
+
+ /** An AttributeDescriptor array that depends on the current UiParent. */
+ private AttributeDescriptor[] mCachedAttributeDescriptors;
+
+ public UiViewElementNode(ViewElementDescriptor elementDescriptor) {
+ super(elementDescriptor);
+ }
+
+ /**
+ * Returns an AttributeDescriptor array that depends on the current UiParent.
+ * <p/>
+ * The array merges both "direct" attributes with the descriptor layout attributes.
+ * The array instance is cached and cleared if the UiParent is changed.
+ */
+ @Override
+ public AttributeDescriptor[] getAttributeDescriptors() {
+ if (!getDescriptor().syncAttributes()) {
+ mCachedAttributeDescriptors = null;
+ }
+ if (mCachedAttributeDescriptors != null) {
+ return mCachedAttributeDescriptors;
+ }
+
+ UiElementNode ui_parent = getUiParent();
+ AttributeDescriptor[] direct_attrs = super.getAttributeDescriptors();
+ mCachedAttributeDescriptors = direct_attrs;
+
+ // Compute layout attributes: These depend on the *parent* this widget is within
+ AttributeDescriptor[] layout_attrs = null;
+ boolean need_xmlns = false;
+
+ if (ui_parent instanceof UiDocumentNode) {
+ // Limitation: right now the layout behaves as if everything was
+ // owned by a FrameLayout.
+ // TODO replace by something user-configurable.
+
+ IProject project = getEditor().getProject();
+ if (project != null) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ LayoutDescriptors descriptors = data.getLayoutDescriptors();
+ ViewElementDescriptor desc =
+ descriptors.findDescriptorByClass(FQCN_FRAME_LAYOUT);
+ if (desc != null) {
+ layout_attrs = desc.getLayoutAttributes();
+ need_xmlns = true;
+ }
+ }
+ }
+ }
+ }
+ } else if (ui_parent instanceof UiViewElementNode) {
+ layout_attrs =
+ ((ViewElementDescriptor) ui_parent.getDescriptor()).getLayoutAttributes();
+ }
+
+ if (layout_attrs == null || layout_attrs.length == 0) {
+ return mCachedAttributeDescriptors;
+ }
+
+ mCachedAttributeDescriptors =
+ new AttributeDescriptor[direct_attrs.length +
+ layout_attrs.length +
+ (need_xmlns ? 1 : 0)];
+ System.arraycopy(direct_attrs, 0,
+ mCachedAttributeDescriptors, 0,
+ direct_attrs.length);
+ System.arraycopy(layout_attrs, 0,
+ mCachedAttributeDescriptors, direct_attrs.length,
+ layout_attrs.length);
+ if (need_xmlns) {
+ AttributeDescriptor desc = new XmlnsAttributeDescriptor(ANDROID_NS_NAME, ANDROID_URI);
+ mCachedAttributeDescriptors[direct_attrs.length + layout_attrs.length] = desc;
+ }
+
+ return mCachedAttributeDescriptors;
+ }
+
+ public Image getIcon() {
+ ElementDescriptor desc = getDescriptor();
+ if (desc != null) {
+ Image img = null;
+ // Special case for the common case of vertical linear layouts:
+ // show vertical linear icon (the default icon shows horizontal orientation)
+ String uiName = desc.getUiName();
+ IconFactory icons = IconFactory.getInstance();
+ if (uiName.equals(LINEAR_LAYOUT)) {
+ Element e = (Element) getXmlNode();
+ if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION))) {
+ IconFactory factory = icons;
+ img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$
+ }
+ } else if (uiName.equals(VIEW_TAG)) {
+ Node xmlNode = getXmlNode();
+ if (xmlNode instanceof Element) {
+ String className = ((Element) xmlNode).getAttribute(ATTR_CLASS);
+ if (className != null && className.length() > 0) {
+ int index = className.lastIndexOf('.');
+ if (index != -1) {
+ className = "customView"; //$NON-NLS-1$
+ }
+ img = icons.getIcon(className);
+ }
+ }
+
+ if (img == null) {
+ // Can't have both view.png and View.png; issues on case sensitive vs
+ // case insensitive file systems
+ img = icons.getIcon("View"); //$NON-NLS-1$
+ }
+ }
+ if (img == null) {
+ img = desc.getGenericIcon();
+ }
+
+ if (img != null) {
+ AndroidXmlEditor editor = getEditor();
+ if (editor != null) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
+ if (delegate != null) {
+ IMarker marker = delegate.getIssueForNode(this);
+ if (marker != null) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, 0);
+ if (severity == IMarker.SEVERITY_ERROR) {
+ return icons.addErrorIcon(img);
+ } else {
+ return icons.addWarningIcon(img);
+ }
+ }
+ }
+ }
+
+ return img;
+ }
+
+ return img;
+ }
+
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Sets the parent of this UI node.
+ * <p/>
+ * Also removes the cached AttributeDescriptor array that depends on the current UiParent.
+ */
+ @Override
+ protected void setUiParent(UiElementNode parent) {
+ super.setUiParent(parent);
+ mCachedAttributeDescriptors = null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java
new file mode 100644
index 000000000..1492adbb7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest;
+
+import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Content Assist Processor for AndroidManifest.xml
+ */
+@VisibleForTesting
+public final class ManifestContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for ManifestContentAssist
+ */
+ public ManifestContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_MANIFEST);
+ }
+
+ @Override
+ protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset,
+ String parentTagName, String attributeName, Node node, String wordPrefix,
+ boolean skipEndTag, int replaceLength) {
+ if (attributeName.endsWith(ATTRIBUTE_MIN_SDK_VERSION)
+ || attributeName.endsWith(ATTRIBUTE_TARGET_SDK_VERSION)) {
+ // The user is completing the minSdkVersion attribute: it should be
+ // an integer for the API version, but we'll add full Android version
+ // names to make it more obvious what they're selecting
+
+ List<Pair<String, String>> choices = new ArrayList<Pair<String, String>>();
+ int max = AdtUtils.getHighestKnownApiLevel();
+ // Look for any more recent installed versions the user may have
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk == null) {
+ return false;
+ }
+ IAndroidTarget[] targets = sdk.getTargets();
+ for (IAndroidTarget target : targets) {
+ AndroidVersion version = target.getVersion();
+ int apiLevel = version.getApiLevel();
+ if (apiLevel > max) {
+ if (version.isPreview()) {
+ // Use codename, not API level, as version string for preview versions
+ choices.add(Pair.of(version.getCodename(), version.getCodename()));
+ } else {
+ choices.add(Pair.of(Integer.toString(apiLevel), target.getFullName()));
+ }
+ }
+ }
+ for (int api = max; api >= 1; api--) {
+ String name = AdtUtils.getAndroidName(api);
+ choices.add(Pair.of(Integer.toString(api), name));
+ }
+ char needTag = 0;
+ addMatchingProposals(proposals, choices.toArray(), offset, node, wordPrefix,
+ needTag, true /* isAttribute */, false /* isNew */,
+ skipEndTag /* skipEndTag */, replaceLength);
+ return true;
+ } else {
+ return super.computeAttributeValues(proposals, offset, parentTagName, attributeName,
+ node, wordPrefix, skipEndTag, replaceLength);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java
new file mode 100644
index 000000000..55ebf5970
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors.USES_PERMISSION;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.manifest.pages.ApplicationPage;
+import com.android.ide.eclipse.adt.internal.editors.manifest.pages.InstrumentationPage;
+import com.android.ide.eclipse.adt.internal.editors.manifest.pages.OverviewPage;
+import com.android.ide.eclipse.adt.internal.editors.manifest.pages.PermissionPage;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Multi-page form editor for AndroidManifest.xml.
+ */
+@SuppressWarnings("restriction")
+public final class ManifestEditor extends AndroidXmlEditor {
+
+ public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".manifest.ManifestEditor"; //$NON-NLS-1$
+
+ private final static String EMPTY = ""; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiElementNode mUiManifestNode;
+ /** The Application Page tab */
+ private ApplicationPage mAppPage;
+ /** The Overview Manifest Page tab */
+ private OverviewPage mOverviewPage;
+ /** The Permission Page tab */
+ private PermissionPage mPermissionPage;
+ /** The Instrumentation Page tab */
+ private InstrumentationPage mInstrumentationPage;
+
+ private IFileListener mMarkerMonitor;
+
+
+ /**
+ * Creates the form editor for AndroidManifest.xml.
+ */
+ public ManifestEditor() {
+ super();
+ addDefaultTargetListener();
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ GlobalProjectMonitor.getMonitor().removeFileListener(mMarkerMonitor);
+ }
+
+ @Override
+ public void activated() {
+ super.activated();
+ clearActionBindings(false);
+ }
+
+ @Override
+ public void deactivated() {
+ super.deactivated();
+ updateActionBindings();
+ }
+
+ @Override
+ protected void pageChange(int newPageIndex) {
+ super.pageChange(newPageIndex);
+ if (newPageIndex == mTextPageIndex) {
+ updateActionBindings();
+ } else {
+ clearActionBindings(false);
+ }
+ }
+
+ @Override
+ protected int getPersistenceCategory() {
+ return CATEGORY_MANIFEST;
+ }
+
+ /**
+ * Return the root node of the UI element hierarchy, which here
+ * is the "manifest" node.
+ */
+ @Override
+ public UiElementNode getUiRootNode() {
+ return mUiManifestNode;
+ }
+
+ /**
+ * Returns the Manifest descriptors for the file being edited.
+ */
+ public AndroidManifestDescriptors getManifestDescriptors() {
+ AndroidTargetData data = getTargetData();
+ if (data != null) {
+ return data.getManifestDescriptors();
+ }
+
+ return null;
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ // Look up the current (pre-save) values of minSdkVersion and targetSdkVersion
+ int prevMinSdkVersion = -1;
+ int prevTargetSdkVersion = -1;
+ IProject project = null;
+ ManifestInfo info = null;
+ try {
+ project = getProject();
+ if (project != null) {
+ info = ManifestInfo.get(project);
+ prevMinSdkVersion = info.getMinSdkVersion();
+ prevTargetSdkVersion = info.getTargetSdkVersion();
+ info.clear();
+ }
+ } catch (Throwable t) {
+ // We don't expect exceptions from the above calls, but we *really*
+ // need to make sure that nothing can prevent the save function from
+ // getting called!
+ AdtPlugin.log(t, null);
+ }
+
+ // Actually save
+ super.doSave(monitor);
+
+ // If the target/minSdkVersion has changed, clear all lint warnings (since many
+ // of them are tied to the min/target sdk levels), in order to avoid showing stale
+ // results
+ try {
+ if (info != null) {
+ int newMinSdkVersion = info.getMinSdkVersion();
+ int newTargetSdkVersion = info.getTargetSdkVersion();
+ if (newMinSdkVersion != prevMinSdkVersion
+ || newTargetSdkVersion != prevTargetSdkVersion) {
+ assert project != null;
+ EclipseLintClient.clearMarkers(project);
+ }
+ }
+ } catch (Throwable t) {
+ AdtPlugin.log(t, null);
+ }
+ }
+
+ /**
+ * Creates the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ addPage(mOverviewPage = new OverviewPage(this));
+ addPage(mAppPage = new ApplicationPage(this));
+ addPage(mPermissionPage = new PermissionPage(this));
+ addPage(mInstrumentationPage = new InstrumentationPage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the project name.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ IFile inputFile = getInputFile();
+ if (inputFile != null) {
+ startMonitoringMarkers();
+ setPartName(String.format("%1$s Manifest", inputFile.getProject().getName()));
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ // create the ui root node on demand.
+ initUiRootNode(false /*force*/);
+
+ loadFromXml(xml_doc);
+ }
+
+ private void loadFromXml(Document xmlDoc) {
+ mUiManifestNode.setXmlDocument(xmlDoc);
+ Node node = getManifestXmlNode(xmlDoc);
+
+ if (node != null) {
+ // Refresh the manifest UI node and all its descendants
+ mUiManifestNode.loadFromXmlNode(node);
+ }
+ }
+
+ private Node getManifestXmlNode(Document xmlDoc) {
+ if (xmlDoc != null) {
+ ElementDescriptor manifestDesc = mUiManifestNode.getDescriptor();
+ String manifestXmlName = manifestDesc == null ? null : manifestDesc.getXmlName();
+ assert manifestXmlName != null;
+
+ if (manifestXmlName != null) {
+ Node node = xmlDoc.getDocumentElement();
+ if (node != null && manifestXmlName.equals(node.getNodeName())) {
+ return node;
+ }
+
+ for (node = xmlDoc.getFirstChild();
+ node != null;
+ node = node.getNextSibling()) {
+ if (node.getNodeType() == Node.ELEMENT_NODE &&
+ manifestXmlName.equals(node.getNodeName())) {
+ return node;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void onDescriptorsChanged() {
+ IStructuredModel model = getModelForRead();
+ if (model != null) {
+ try {
+ Node node = getManifestXmlNode(getXmlDocument(model));
+ mUiManifestNode.reloadFromXmlNode(node);
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ if (mOverviewPage != null) {
+ mOverviewPage.refreshUiApplicationNode();
+ }
+
+ if (mAppPage != null) {
+ mAppPage.refreshUiApplicationNode();
+ }
+
+ if (mPermissionPage != null) {
+ mPermissionPage.refreshUiNode();
+ }
+
+ if (mInstrumentationPage != null) {
+ mInstrumentationPage.refreshUiNode();
+ }
+ }
+
+ /**
+ * Reads and processes the current markers and adds a listener for marker changes.
+ */
+ private void startMonitoringMarkers() {
+ final IFile inputFile = getInputFile();
+ if (inputFile != null) {
+ updateFromExistingMarkers(inputFile);
+
+ mMarkerMonitor = new IFileListener() {
+ @Override
+ public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
+ int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
+ if (isAndroidProject && file.equals(inputFile)) {
+ processMarkerChanges(markerDeltas);
+ }
+ }
+ };
+
+ GlobalProjectMonitor.getMonitor().addFileListener(
+ mMarkerMonitor, IResourceDelta.CHANGED);
+ }
+ }
+
+ /**
+ * Processes the markers of the specified {@link IFile} and updates the error status of
+ * {@link UiElementNode}s and {@link UiAttributeNode}s.
+ * @param inputFile the file being edited.
+ */
+ private void updateFromExistingMarkers(IFile inputFile) {
+ try {
+ // get the markers for the file
+ IMarker[] markers = inputFile.findMarkers(
+ AdtConstants.MARKER_ANDROID, true, IResource.DEPTH_ZERO);
+
+ AndroidManifestDescriptors desc = getManifestDescriptors();
+ if (desc != null) {
+ ElementDescriptor appElement = desc.getApplicationElement();
+
+ if (appElement != null && mUiManifestNode != null) {
+ UiElementNode appUiNode = mUiManifestNode.findUiChildNode(
+ appElement.getXmlName());
+ List<UiElementNode> children = appUiNode.getUiChildren();
+
+ for (IMarker marker : markers) {
+ processMarker(marker, children, IResourceDelta.ADDED);
+ }
+ }
+ }
+
+ } catch (CoreException e) {
+ // findMarkers can throw an exception, in which case, we'll do nothing.
+ }
+ }
+
+ /**
+ * Processes a {@link IMarker} change.
+ * @param markerDeltas the list of {@link IMarkerDelta}
+ */
+ private void processMarkerChanges(IMarkerDelta[] markerDeltas) {
+ AndroidManifestDescriptors descriptors = getManifestDescriptors();
+ if (descriptors != null && descriptors.getApplicationElement() != null) {
+ UiElementNode app_ui_node = mUiManifestNode.findUiChildNode(
+ descriptors.getApplicationElement().getXmlName());
+ List<UiElementNode> children = app_ui_node.getUiChildren();
+
+ for (IMarkerDelta markerDelta : markerDeltas) {
+ processMarker(markerDelta.getMarker(), children, markerDelta.getKind());
+ }
+ }
+ }
+
+ /**
+ * Processes a new/old/updated marker.
+ * @param marker The marker being added/removed/changed
+ * @param nodeList the list of activity/service/provider/receiver nodes.
+ * @param kind the change kind. Can be {@link IResourceDelta#ADDED},
+ * {@link IResourceDelta#REMOVED}, or {@link IResourceDelta#CHANGED}
+ */
+ private void processMarker(IMarker marker, List<UiElementNode> nodeList, int kind) {
+ // get the data from the marker
+ String nodeType = marker.getAttribute(AdtConstants.MARKER_ATTR_TYPE, EMPTY);
+ if (nodeType == EMPTY) {
+ return;
+ }
+
+ String className = marker.getAttribute(AdtConstants.MARKER_ATTR_CLASS, EMPTY);
+ if (className == EMPTY) {
+ return;
+ }
+
+ for (UiElementNode ui_node : nodeList) {
+ if (ui_node.getDescriptor().getXmlName().equals(nodeType)) {
+ for (UiAttributeNode attr : ui_node.getAllUiAttributes()) {
+ if (attr.getDescriptor().getXmlLocalName().equals(
+ AndroidManifestDescriptors.ANDROID_NAME_ATTR)) {
+ if (attr.getCurrentValue().equals(className)) {
+ if (kind == IResourceDelta.REMOVED) {
+ attr.setHasError(false);
+ } else {
+ attr.setHasError(true);
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiManifestNode is recreated even if it already exists.
+ */
+ @Override
+ protected void initUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (mUiManifestNode != null && force == false) {
+ return;
+ }
+
+ AndroidManifestDescriptors manifestDescriptor = getManifestDescriptors();
+
+ if (manifestDescriptor != null) {
+ ElementDescriptor manifestElement = manifestDescriptor.getManifestElement();
+ mUiManifestNode = manifestElement.createUiNode();
+ mUiManifestNode.setEditor(this);
+
+ // Similarly, always create the /manifest/uses-sdk followed by /manifest/application
+ // (order of the elements now matters)
+ ElementDescriptor element = manifestDescriptor.getUsesSdkElement();
+ boolean present = false;
+ for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) {
+ if (ui_node.getDescriptor() == element) {
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ mUiManifestNode.appendNewUiChild(element);
+ }
+
+ element = manifestDescriptor.getApplicationElement();
+ present = false;
+ for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) {
+ if (ui_node.getDescriptor() == element) {
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ mUiManifestNode.appendNewUiChild(element);
+ }
+
+ onDescriptorsChanged();
+ } else {
+ // create a dummy descriptor/uinode until we have real descriptors
+ ElementDescriptor desc = new ElementDescriptor("manifest", //$NON-NLS-1$
+ "temporary descriptors due to missing decriptors", //$NON-NLS-1$
+ null /*tooltip*/, null /*sdk_url*/, null /*attributes*/,
+ null /*children*/, false /*mandatory*/);
+ mUiManifestNode = desc.createUiNode();
+ mUiManifestNode.setEditor(this);
+ }
+ }
+
+ /**
+ * Adds the given set of permissions into the manifest file in the suitable
+ * location
+ *
+ * @param permissions permission fqcn's to be added
+ * @param show if true, show one or more of the newly added permissions
+ */
+ public void addPermissions(@NonNull final List<String> permissions, final boolean show) {
+ wrapUndoEditXmlModel("Add permissions", new Runnable() {
+ @Override
+ public void run() {
+ // Ensure that the model is current:
+ initUiRootNode(true /*force*/);
+ UiElementNode root = getUiRootNode();
+
+ ElementDescriptor descriptor = getManifestDescriptors().getUsesPermissionElement();
+ boolean shown = false;
+ for (String permission : permissions) {
+ // Find the first permission which sorts alphabetically laster than
+ // this permission (or the last permission, if none are after in the alphabet)
+ // and insert it there
+ int lastPermissionIndex = -1;
+ int nextPermissionIndex = -1;
+ int index = 0;
+ for (UiElementNode sibling : root.getUiChildren()) {
+ Node node = sibling.getXmlNode();
+ if (node.getNodeName().equals(USES_PERMISSION)) {
+ lastPermissionIndex = index;
+ String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME);
+ if (permission.compareTo(name) < 0) {
+ nextPermissionIndex = index;
+ break;
+ }
+ } else if (node.getNodeName().equals("application")) { //$NON-NLS-1$
+ // permissions should come before the application element
+ nextPermissionIndex = index;
+ break;
+ }
+ index++;
+ }
+
+ if (nextPermissionIndex != -1) {
+ index = nextPermissionIndex;
+ } else if (lastPermissionIndex != -1) {
+ index = lastPermissionIndex + 1;
+ } else {
+ index = root.getUiChildren().size();
+ }
+ UiElementNode usesPermission = root.insertNewUiChild(index, descriptor);
+ usesPermission.setAttributeValue(ATTR_NAME, ANDROID_URI, permission,
+ true /*override*/);
+ Node node = usesPermission.createXmlNode();
+ if (show && !shown) {
+ shown = true;
+ if (node instanceof IndexedRegion && getInputFile() != null) {
+ IndexedRegion indexedRegion = (IndexedRegion) node;
+ IRegion region = new Region(indexedRegion.getStartOffset(),
+ indexedRegion.getEndOffset() - indexedRegion.getStartOffset());
+ try {
+ AdtPlugin.openFile(getInputFile(), region, true /*show*/);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ } else {
+ show(node);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Removes the permissions from the manifest editor
+ *
+ * @param permissions the permission fqcn's to be removed
+ */
+ public void removePermissions(@NonNull final Collection<String> permissions) {
+ wrapUndoEditXmlModel("Remove permissions", new Runnable() {
+ @Override
+ public void run() {
+ // Ensure that the model is current:
+ initUiRootNode(true /*force*/);
+ UiElementNode root = getUiRootNode();
+
+ for (String permission : permissions) {
+ for (UiElementNode sibling : root.getUiChildren()) {
+ Node node = sibling.getXmlNode();
+ if (node.getNodeName().equals(USES_PERMISSION)) {
+ String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME);
+ if (name.equals(permission)) {
+ sibling.deleteXmlNode();
+ break;
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java
new file mode 100644
index 000000000..8beca30b8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.ide.IDEActionFactory;
+import org.eclipse.ui.part.MultiPageEditorActionBarContributor;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ITextEditorActionConstants;
+
+/**
+ * Manages the installation/deinstallation of global actions for multi-page
+ * editors. Responsible for the redirection of global actions to the active
+ * editor. Multi-page contributor replaces the contributors for the individual
+ * editors in the multi-page editor.
+ *
+ * TODO: Doesn't look like we need this. Remove it if not needed.
+ * @deprecated
+ */
+final class ManifestEditorContributor extends MultiPageEditorActionBarContributor {
+ private IEditorPart mActiveEditorPart;
+
+ /**
+ * Creates a multi-page contributor.
+ *
+ * Marked as Private so it can't be instanciated. This is a cheap way to make sure
+ * it's not being used. As noted in constructor, should be removed if not used.
+ * @deprecated
+ */
+ private ManifestEditorContributor() {
+ super();
+ }
+
+ /**
+ * Returns the action registed with the given text editor.
+ *
+ * @return IAction or null if editor is null.
+ */
+ protected IAction getAction(ITextEditor editor, String actionID) {
+ return (editor == null ? null : editor.getAction(actionID));
+ }
+
+ /*
+ * (non-JavaDoc) Method declared in
+ * AbstractMultiPageEditorActionBarContributor.
+ */
+
+ @Override
+ public void setActivePage(IEditorPart part) {
+ if (mActiveEditorPart == part)
+ return;
+
+ mActiveEditorPart = part;
+
+ IActionBars actionBars = getActionBars();
+ if (actionBars != null) {
+
+ ITextEditor editor =
+ (part instanceof ITextEditor) ? (ITextEditor)part : null;
+
+ actionBars.setGlobalActionHandler(ActionFactory.DELETE.getId(),
+ getAction(editor, ITextEditorActionConstants.DELETE));
+ actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(),
+ getAction(editor, ITextEditorActionConstants.UNDO));
+ actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(),
+ getAction(editor, ITextEditorActionConstants.REDO));
+ actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(),
+ getAction(editor, ITextEditorActionConstants.CUT));
+ actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(),
+ getAction(editor, ITextEditorActionConstants.COPY));
+ actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(),
+ getAction(editor, ITextEditorActionConstants.PASTE));
+ actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ getAction(editor, ITextEditorActionConstants.SELECT_ALL));
+ actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(),
+ getAction(editor, ITextEditorActionConstants.FIND));
+ actionBars.setGlobalActionHandler(
+ IDEActionFactory.BOOKMARK.getId(), getAction(editor,
+ IDEActionFactory.BOOKMARK.getId()));
+ actionBars.updateActionBars();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java
new file mode 100644
index 000000000..6d2d1c1f2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java
@@ -0,0 +1,957 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.CLASS_ACTIVITY;
+import static com.android.SdkConstants.NS_RESOURCES;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_ICON;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_LABEL;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_SUPPORTS_RTL;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_THEME;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_UI_OPTIONS;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_VALUE;
+import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
+import static com.android.xml.AndroidManifest.NODE_METADATA;
+import static com.android.xml.AndroidManifest.NODE_USES_SDK;
+import static com.android.xml.AndroidManifest.VALUE_PARENT_ACTIVITY;
+import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFolderWrapper;
+import com.android.io.IAbstractFile;
+import com.android.io.StreamException;
+import com.android.resources.ScreenSize;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+import com.android.xml.AndroidManifest;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jdt.core.IField;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+import org.eclipse.jdt.internal.core.BinaryType;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.ui.editors.text.TextFileDocumentProvider;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Retrieves and caches manifest information such as the themes to be used for
+ * a given activity.
+ *
+ * @see AndroidManifest
+ */
+public class ManifestInfo {
+
+ public static class ActivityAttributes {
+ @Nullable
+ private final String mIcon;
+ @Nullable
+ private final String mLabel;
+ @NonNull
+ private final String mName;
+ @Nullable
+ private final String mParentActivity;
+ @Nullable
+ private final String mTheme;
+ @Nullable
+ private final String mUiOptions;
+
+ public ActivityAttributes(Element activity, String packageName) {
+
+ // Get activity name.
+ String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME);
+ if (name == null || name.length() == 0) {
+ throw new RuntimeException("Activity name cannot be empty");
+ }
+ int index = name.indexOf('.');
+ if (index <= 0 && packageName != null && !packageName.isEmpty()) {
+ name = packageName + (index == -1 ? "." : "") + name;
+ }
+ mName = name;
+
+ // Get activity icon.
+ String value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON);
+ if (value != null && value.length() > 0) {
+ mIcon = value;
+ } else {
+ mIcon = null;
+ }
+
+ // Get activity label.
+ value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL);
+ if (value != null && value.length() > 0) {
+ mLabel = value;
+ } else {
+ mLabel = null;
+ }
+
+ // Get activity parent. Also search the meta-data for parent info.
+ value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_PARENT_ACTIVITY_NAME);
+ if (value == null || value.length() == 0) {
+ // TODO: Not sure if meta data can be used for API Level > 16
+ NodeList metaData = activity.getElementsByTagName(NODE_METADATA);
+ for (int j = 0, m = metaData.getLength(); j < m; j++) {
+ Element data = (Element) metaData.item(j);
+ String metadataName = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME);
+ if (VALUE_PARENT_ACTIVITY.equals(metadataName)) {
+ value = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_VALUE);
+ if (value != null) {
+ index = value.indexOf('.');
+ if (index <= 0 && packageName != null && !packageName.isEmpty()) {
+ value = packageName + (index == -1 ? "." : "") + value;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (value != null && value.length() > 0) {
+ mParentActivity = value;
+ } else {
+ mParentActivity = null;
+ }
+
+ // Get activity theme.
+ value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME);
+ if (value != null && value.length() > 0) {
+ mTheme = value;
+ } else {
+ mTheme = null;
+ }
+
+ // Get UI options.
+ value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_UI_OPTIONS);
+ if (value != null && value.length() > 0) {
+ mUiOptions = value;
+ } else {
+ mUiOptions = null;
+ }
+ }
+
+ @Nullable
+ public String getIcon() {
+ return mIcon;
+ }
+
+ @Nullable
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ @Nullable
+ public String getParentActivity() {
+ return mParentActivity;
+ }
+
+ @Nullable
+ public String getTheme() {
+ return mTheme;
+ }
+
+ @Nullable
+ public String getUiOptions() {
+ return mUiOptions;
+ }
+ }
+
+ /**
+ * The maximum number of milliseconds to search for an activity in the codebase when
+ * attempting to associate layouts with activities in
+ * {@link #guessActivity(IFile, String)}
+ */
+ private static final int SEARCH_TIMEOUT_MS = 3000;
+
+ private final IProject mProject;
+ private String mPackage;
+ private String mManifestTheme;
+ private Map<String, ActivityAttributes> mActivityAttributes;
+ private IAbstractFile mManifestFile;
+ private long mLastModified;
+ private long mLastChecked;
+ private String mMinSdkName;
+ private int mMinSdk;
+ private int mTargetSdk;
+ private String mApplicationIcon;
+ private String mApplicationLabel;
+ private boolean mApplicationSupportsRtl;
+
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link ManifestInfo} for this project
+ */
+ final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "manifest"); //$NON-NLS-1$
+
+ /**
+ * Constructs an {@link ManifestInfo} for the given project. Don't use this method;
+ * use the {@link #get} factory method instead.
+ *
+ * @param project project to create an {@link ManifestInfo} for
+ */
+ private ManifestInfo(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Clears the cached manifest information. The next get call on one of the
+ * properties will cause the information to be refreshed.
+ */
+ public void clear() {
+ mLastChecked = 0;
+ }
+
+ /**
+ * Returns the {@link ManifestInfo} for the given project
+ *
+ * @param project the project the finder is associated with
+ * @return a {@ManifestInfo} for the given project, never null
+ */
+ @NonNull
+ public static ManifestInfo get(IProject project) {
+ ManifestInfo finder = null;
+ try {
+ finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (finder == null) {
+ finder = new ManifestInfo(project);
+ try {
+ project.setSessionProperty(MANIFEST_FINDER, finder);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store ManifestInfo");
+ }
+ }
+
+ return finder;
+ }
+
+ /**
+ * Ensure that the package, theme and activity maps are initialized and up to date
+ * with respect to the manifest file
+ */
+ private void sync() {
+ // Since each of the accessors call sync(), allow a bunch of immediate
+ // accessors to all bypass the file stat() below
+ long now = System.currentTimeMillis();
+ if (now - mLastChecked < 50 && mManifestFile != null) {
+ return;
+ }
+ mLastChecked = now;
+
+ if (mManifestFile == null) {
+ IFolderWrapper projectFolder = new IFolderWrapper(mProject);
+ mManifestFile = AndroidManifest.getManifest(projectFolder);
+ if (mManifestFile == null) {
+ return;
+ }
+ }
+
+ // Check to see if our data is up to date
+ long fileModified = mManifestFile.getModificationStamp();
+ if (fileModified == mLastModified) {
+ // Already have up to date data
+ return;
+ }
+ mLastModified = fileModified;
+
+ mActivityAttributes = new HashMap<String, ActivityAttributes>();
+ mManifestTheme = null;
+ mTargetSdk = 1; // Default when not specified
+ mMinSdk = 1; // Default when not specified
+ mMinSdkName = "1"; // Default when not specified
+ mPackage = ""; //$NON-NLS-1$
+ mApplicationIcon = null;
+ mApplicationLabel = null;
+ mApplicationSupportsRtl = false;
+
+ Document document = null;
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ InputSource is = new InputSource(mManifestFile.getContents());
+
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ document = builder.parse(is);
+
+ Element root = document.getDocumentElement();
+ mPackage = root.getAttribute(ATTRIBUTE_PACKAGE);
+ NodeList activities = document.getElementsByTagName(NODE_ACTIVITY);
+ for (int i = 0, n = activities.getLength(); i < n; i++) {
+ Element activity = (Element) activities.item(i);
+ ActivityAttributes info = new ActivityAttributes(activity, mPackage);
+ mActivityAttributes.put(info.getName(), info);
+ }
+
+ NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION);
+ if (applications.getLength() > 0) {
+ assert applications.getLength() == 1;
+ Element application = (Element) applications.item(0);
+ if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) {
+ mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON);
+ }
+ if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) {
+ mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL);
+ }
+ if (SdkConstants.VALUE_TRUE.equals(application.getAttributeNS(NS_RESOURCES,
+ ATTRIBUTE_SUPPORTS_RTL))) {
+ mApplicationSupportsRtl = true;
+ }
+
+ String defaultTheme = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME);
+ if (defaultTheme != null && !defaultTheme.isEmpty()) {
+ // From manifest theme documentation:
+ // "If that attribute is also not set, the default system theme is used."
+ mManifestTheme = defaultTheme;
+ }
+ }
+
+ // Look up target SDK
+ NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK);
+ if (usesSdks.getLength() > 0) {
+ Element usesSdk = (Element) usesSdks.item(0);
+ mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1);
+ mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk);
+ }
+
+ } catch (SAXException e) {
+ AdtPlugin.log(e, "Malformed manifest");
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Could not read Manifest data");
+ }
+ }
+
+ private int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) {
+ String valueString = null;
+ if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) {
+ valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute);
+ if (attribute.equals(ATTRIBUTE_MIN_SDK_VERSION)) {
+ mMinSdkName = valueString;
+ }
+ }
+
+ if (valueString != null) {
+ int apiLevel = -1;
+ try {
+ apiLevel = Integer.valueOf(valueString);
+ } catch (NumberFormatException e) {
+ // Handle codename
+ if (Sdk.getCurrent() != null) {
+ IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
+ "android-" + valueString); //$NON-NLS-1$
+ if (target != null) {
+ // codename future API level is current api + 1
+ apiLevel = target.getVersion().getApiLevel() + 1;
+ }
+ }
+ }
+
+ return apiLevel;
+ }
+
+ return defaultApiLevel;
+ }
+
+ /**
+ * Returns the default package registered in the Android manifest
+ *
+ * @return the default package registered in the manifest
+ */
+ @NonNull
+ public String getPackage() {
+ sync();
+ return mPackage;
+ }
+
+ /**
+ * Returns a map from activity full class names to the corresponding {@link ActivityAttributes}.
+ *
+ * @return a map from activity fqcn to ActivityAttributes
+ */
+ @NonNull
+ public Map<String, ActivityAttributes> getActivityAttributesMap() {
+ sync();
+ return mActivityAttributes;
+ }
+
+ /**
+ * Returns the attributes of an activity given its full class name.
+ */
+ @Nullable
+ public ActivityAttributes getActivityAttributes(String activity) {
+ return getActivityAttributesMap().get(activity);
+ }
+
+ /**
+ * Returns the manifest theme registered on the application, if any
+ *
+ * @return a manifest theme, or null if none was registered
+ */
+ @Nullable
+ public String getManifestTheme() {
+ sync();
+ return mManifestTheme;
+ }
+
+ /**
+ * Returns the default theme for this project, by looking at the manifest default
+ * theme registration, target SDK, rendering target, etc.
+ *
+ * @param renderingTarget the rendering target use to render the theme, or null
+ * @param screenSize the screen size to obtain a default theme for, or null if unknown
+ * @return the theme to use for this project, never null
+ */
+ @NonNull
+ public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) {
+ sync();
+
+ if (mManifestTheme != null) {
+ return mManifestTheme;
+ }
+
+ int renderingTargetSdk = mTargetSdk;
+ if (renderingTarget != null) {
+ renderingTargetSdk = renderingTarget.getVersion().getApiLevel();
+ }
+
+ int apiLevel = Math.min(mTargetSdk, renderingTargetSdk);
+ // For now this theme works only on XLARGE screens. When it works for all sizes,
+ // add that new apiLevel to this check.
+ if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE || apiLevel >= 14) {
+ return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$
+ } else {
+ return ANDROID_STYLE_RESOURCE_PREFIX + "Theme"; //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Returns the application icon, or null
+ *
+ * @return the application icon, or null
+ */
+ @Nullable
+ public String getApplicationIcon() {
+ sync();
+ return mApplicationIcon;
+ }
+
+ /**
+ * Returns the application label, or null
+ *
+ * @return the application label, or null
+ */
+ @Nullable
+ public String getApplicationLabel() {
+ sync();
+ return mApplicationLabel;
+ }
+
+ /**
+ * Returns true if the application has RTL support.
+ *
+ * @return true if the application has RTL support.
+ */
+ public boolean isRtlSupported() {
+ sync();
+ return mApplicationSupportsRtl;
+ }
+
+ /**
+ * Returns the target SDK version
+ *
+ * @return the target SDK version
+ */
+ public int getTargetSdkVersion() {
+ sync();
+ return mTargetSdk;
+ }
+
+ /**
+ * Returns the minimum SDK version
+ *
+ * @return the minimum SDK version
+ */
+ public int getMinSdkVersion() {
+ sync();
+ return mMinSdk;
+ }
+
+ /**
+ * Returns the minimum SDK version name (which may not be a numeric string, e.g.
+ * it could be a codename). It will never be null or empty; if no min sdk version
+ * was specified in the manifest, the return value will be "1". Use
+ * {@link #getMinSdkCodeName()} instead if you want to look up whether there is a code name.
+ *
+ * @return the minimum SDK version
+ */
+ @NonNull
+ public String getMinSdkName() {
+ sync();
+ if (mMinSdkName == null || mMinSdkName.isEmpty()) {
+ mMinSdkName = "1"; //$NON-NLS-1$
+ }
+
+ return mMinSdkName;
+ }
+
+ /**
+ * Returns the code name used for the minimum SDK version, if any.
+ *
+ * @return the minSdkVersion codename or null
+ */
+ @Nullable
+ public String getMinSdkCodeName() {
+ String minSdkName = getMinSdkName();
+ if (!Character.isDigit(minSdkName.charAt(0))) {
+ return minSdkName;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link IPackageFragment} for the package registered in the manifest
+ *
+ * @return the {@link IPackageFragment} for the package registered in the manifest
+ */
+ @Nullable
+ public IPackageFragment getPackageFragment() {
+ sync();
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
+ if (javaProject != null) {
+ IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject);
+ if (root != null) {
+ return root.getPackageFragment(mPackage);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the activity associated with the given layout file. Makes an educated guess
+ * by peeking at the usages of the R.layout.name field corresponding to the layout and
+ * if it finds a usage.
+ *
+ * @param project the project containing the layout
+ * @param layoutName the layout whose activity we want to look up
+ * @param pkg the package containing activities
+ * @return the activity name
+ */
+ @Nullable
+ public static String guessActivity(IProject project, String layoutName, String pkg) {
+ List<String> activities = guessActivities(project, layoutName, pkg);
+ if (activities.size() > 0) {
+ return activities.get(0);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the activities associated with the given layout file. Makes an educated guess
+ * by peeking at the usages of the R.layout.name field corresponding to the layout and
+ * if it finds a usage.
+ *
+ * @param project the project containing the layout
+ * @param layoutName the layout whose activity we want to look up
+ * @param pkg the package containing activities
+ * @return the activity name
+ */
+ @NonNull
+ public static List<String> guessActivities(IProject project, String layoutName, String pkg) {
+ final LinkedList<String> activities = new LinkedList<String>();
+ SearchRequestor requestor = new SearchRequestor() {
+ @Override
+ public void acceptSearchMatch(SearchMatch match) throws CoreException {
+ Object element = match.getElement();
+ if (element instanceof IMethod) {
+ IMethod method = (IMethod) element;
+ IType declaringType = method.getDeclaringType();
+ String fqcn = declaringType.getFullyQualifiedName();
+
+ if ((declaringType.getSuperclassName() != null &&
+ declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$
+ || method.getElementName().equals("onCreate")) { //$NON-NLS-1$
+ activities.addFirst(fqcn);
+ } else {
+ activities.addLast(fqcn);
+ }
+ }
+ }
+ };
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject == null) {
+ return Collections.emptyList();
+ }
+ // TODO - look around a bit more and see if we can figure out whether the
+ // call if from within a setContentView call!
+
+ // Search for which java classes call setContentView(R.layout.layoutname);
+ String typeFqcn = "R.layout"; //$NON-NLS-1$
+ if (pkg != null) {
+ typeFqcn = pkg + '.' + typeFqcn;
+ }
+
+ IType type = javaProject.findType(typeFqcn);
+ if (type != null) {
+ IField field = type.getField(layoutName);
+ if (field.exists()) {
+ SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES);
+ try {
+ search(requestor, javaProject, pattern);
+ } catch (OperationCanceledException canceled) {
+ // pass
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return activities;
+ }
+
+ /**
+ * Returns all activities found in the given project (including those in libraries,
+ * except for android.jar itself)
+ *
+ * @param project the project
+ * @return a list of activity classes as fully qualified class names
+ */
+ @SuppressWarnings("restriction") // BinaryType
+ @NonNull
+ public static List<String> getProjectActivities(IProject project) {
+ final List<String> activities = new ArrayList<String>();
+ try {
+ final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ IType[] activityTypes = new IType[0];
+ IType activityType = javaProject.findType(CLASS_ACTIVITY);
+ if (activityType != null) {
+ ITypeHierarchy hierarchy =
+ activityType.newTypeHierarchy(javaProject, new NullProgressMonitor());
+ activityTypes = hierarchy.getAllSubtypes(activityType);
+ for (IType type : activityTypes) {
+ if (type instanceof BinaryType && (type.getClassFile() == null
+ || type.getClassFile().getResource() == null)) {
+ continue;
+ }
+ activities.add(type.getFullyQualifiedName());
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return activities;
+ }
+
+
+ /**
+ * Returns the activity associated with the given layout file.
+ * <p>
+ * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas
+ * guessActivity simply looks for references to "R.layout.foo", this method searches
+ * for all usages of Activity#setContentView(int), and for each match it looks up the
+ * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses
+ * a regexp to pull out "foo" from this, and stores the association that layout "foo"
+ * is associated with the activity class that contained the setContentView call.
+ * <p>
+ * This has two potential advantages:
+ * <ol>
+ * <li>It can be faster. We do the reference search -once-, and we've built a map of
+ * all the layout-to-activity mappings which we can then immediately look up other
+ * layouts for, which is particularly useful at startup when we have to compute the
+ * layout activity associations to populate the theme choosers.
+ * <li>It can be more accurate. Just because an activity references an "R.layout.foo"
+ * field doesn't mean it's setting it as a content view.
+ * </ol>
+ * However, this second advantage is also its chief problem. There are some common
+ * code constructs which means that the associated layout is not explicitly referenced
+ * in a direct setContentView call; on a couple of sample projects I tested I found
+ * patterns like for example "setContentView(v)" where "v" had been computed earlier.
+ * Therefore, for now we're going to stick with the more general approach of just
+ * looking up each field when needed. We're keeping the code around, though statically
+ * compiled out with the "if (false)" construct below in case we revisit this.
+ *
+ * @param layoutFile the layout whose activity we want to look up
+ * @return the activity name
+ */
+ @SuppressWarnings("all")
+ @Nullable
+ public String guessActivityBySetContentView(String layoutName) {
+ if (false) {
+ // These should be fields
+ final Pattern LAYOUT_FIELD_PATTERN =
+ Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$
+ Map<String, String> mUsages = null;
+
+ sync();
+ if (mUsages == null) {
+ final Map<String, String> usages = new HashMap<String, String>();
+ mUsages = usages;
+ SearchRequestor requestor = new SearchRequestor() {
+ @Override
+ public void acceptSearchMatch(SearchMatch match) throws CoreException {
+ Object element = match.getElement();
+ if (element instanceof IMethod) {
+ IMethod method = (IMethod) element;
+ IType declaringType = method.getDeclaringType();
+ String fqcn = declaringType.getFullyQualifiedName();
+ IDocumentProvider provider = new TextFileDocumentProvider();
+ IResource resource = match.getResource();
+ try {
+ provider.connect(resource);
+ IDocument document = provider.getDocument(resource);
+ if (document != null) {
+ String matchText = document.get(match.getOffset(),
+ match.getLength());
+ Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText);
+ if (matcher.find()) {
+ usages.put(matcher.group(1), fqcn);
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't find range information for %1$s",
+ resource.getName());
+ } finally {
+ provider.disconnect(resource);
+ }
+ }
+ }
+ };
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
+ if (javaProject == null) {
+ return null;
+ }
+
+ // Search for which java classes call setContentView(R.layout.layoutname);
+ String typeFqcn = "R.layout"; //$NON-NLS-1$
+ if (mPackage != null) {
+ typeFqcn = mPackage + '.' + typeFqcn;
+ }
+
+ IType activityType = javaProject.findType(CLASS_ACTIVITY);
+ if (activityType != null) {
+ IMethod method = activityType.getMethod(
+ "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$
+ if (method.exists()) {
+ SearchPattern pattern = SearchPattern.createPattern(method,
+ REFERENCES);
+ search(requestor, javaProject, pattern);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return mUsages.get(layoutName);
+ }
+
+ return null;
+ }
+
+ /**
+ * Performs a search using the given pattern, scope and handler. The search will abort
+ * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds.
+ */
+ private static void search(SearchRequestor requestor, IJavaProject javaProject,
+ SearchPattern pattern) throws CoreException {
+ // Find the package fragment specified in the manifest; the activities should
+ // live there.
+ IJavaSearchScope scope = createPackageScope(javaProject);
+
+ SearchParticipant[] participants = new SearchParticipant[] {
+ SearchEngine.getDefaultSearchParticipant()
+ };
+ SearchEngine engine = new SearchEngine();
+
+ final long searchStart = System.currentTimeMillis();
+ NullProgressMonitor monitor = new NullProgressMonitor() {
+ private boolean mCancelled;
+ @Override
+ public void internalWorked(double work) {
+ long searchEnd = System.currentTimeMillis();
+ if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) {
+ mCancelled = true;
+ }
+ }
+
+ @Override
+ public boolean isCanceled() {
+ return mCancelled;
+ }
+ };
+ engine.search(pattern, participants, scope, requestor, monitor);
+ }
+
+ /** Creates a package search scope for the first package root in the given java project */
+ private static IJavaSearchScope createPackageScope(IJavaProject javaProject) {
+ IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject);
+
+ IJavaSearchScope scope;
+ if (packageRoot != null) {
+ IJavaElement[] scopeElements = new IJavaElement[] { packageRoot };
+ scope = SearchEngine.createJavaSearchScope(scopeElements);
+ } else {
+ scope = SearchEngine.createWorkspaceScope();
+ }
+ return scope;
+ }
+
+ /**
+ * Returns the first package root for the given java project
+ *
+ * @param javaProject the project to search in
+ * @return the first package root, or null
+ */
+ @Nullable
+ public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) {
+ IPackageFragmentRoot packageRoot = null;
+ List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject);
+
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ for (IPath path : sources) {
+ IResource firstSource = workspace.getRoot().findMember(path);
+ if (firstSource != null) {
+ packageRoot = javaProject.getPackageFragmentRoot(firstSource);
+ if (packageRoot != null) {
+ break;
+ }
+ }
+ }
+ return packageRoot;
+ }
+
+ /**
+ * Computes the minimum SDK and target SDK versions for the project
+ *
+ * @param project the project to look up the versions for
+ * @return a pair of (minimum SDK, target SDK) versions, never null
+ */
+ @NonNull
+ public static Pair<Integer, Integer> computeSdkVersions(IProject project) {
+ int mMinSdkVersion = 1;
+ int mTargetSdkVersion = 1;
+
+ IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project));
+ if (manifestFile != null) {
+ try {
+ Object value = AndroidManifest.getMinSdkVersion(manifestFile);
+ mMinSdkVersion = 1; // Default case if missing
+ if (value instanceof Integer) {
+ mMinSdkVersion = ((Integer) value).intValue();
+ } else if (value instanceof String) {
+ // handle codename, only if we can resolve it.
+ if (Sdk.getCurrent() != null) {
+ IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
+ "android-" + value); //$NON-NLS-1$
+ if (target != null) {
+ // codename future API level is current api + 1
+ mMinSdkVersion = target.getVersion().getApiLevel() + 1;
+ }
+ }
+ }
+
+ value = AndroidManifest.getTargetSdkVersion(manifestFile);
+ if (value == null) {
+ mTargetSdkVersion = mMinSdkVersion;
+ } else if (value instanceof String) {
+ // handle codename, only if we can resolve it.
+ if (Sdk.getCurrent() != null) {
+ IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
+ "android-" + value); //$NON-NLS-1$
+ if (target != null) {
+ // codename future API level is current api + 1
+ mTargetSdkVersion = target.getVersion().getApiLevel() + 1;
+ }
+ }
+ }
+ } catch (XPathExpressionException e) {
+ // do nothing we'll use 1 below.
+ } catch (StreamException e) {
+ // do nothing we'll use 1 below.
+ }
+ }
+
+ return Pair.of(mMinSdkVersion, mTargetSdkVersion);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java
new file mode 100644
index 000000000..a3d398657
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest;
+
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidSourceViewerConfig;
+
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.source.ISourceViewer;
+
+/**
+ * Source Viewer Configuration that calls in ManifestContentAssist.
+ */
+public final class ManifestSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ private ManifestContentAssist mAndroidContentAssist;
+
+ public ManifestSourceViewerConfig() {
+ super();
+ mAndroidContentAssist = new ManifestContentAssist();
+ }
+
+ @Override
+ public IContentAssistProcessor getAndroidContentAssistProcessor(
+ ISourceViewer sourceViewer,
+ String partitionType) {
+ return mAndroidContentAssist;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java
new file mode 100644
index 000000000..3429e43a0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.AttrsXmlParser;
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+
+import org.eclipse.core.runtime.IStatus;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeSet;
+
+
+/**
+ * Complete description of the AndroidManifest.xml structure.
+ * <p/>
+ * The root element are static instances which always exists.
+ * However their sub-elements and attributes are created only when the SDK changes or is
+ * loaded the first time.
+ */
+public final class AndroidManifestDescriptors implements IDescriptorProvider {
+ /** Name of the {@code <uses-permission>} */
+ public static final String USES_PERMISSION = "uses-permission"; //$NON-NLS-1$
+ private static final String MANIFEST_NODE_NAME = "manifest"; //$NON-NLS-1$
+ private static final String ANDROID_MANIFEST_STYLEABLE =
+ AttrsXmlParser.ANDROID_MANIFEST_STYLEABLE;
+
+ // Public attributes names, attributes descriptors and elements descriptors
+
+ public static final String ANDROID_LABEL_ATTR = "label"; //$NON-NLS-1$
+ public static final String ANDROID_NAME_ATTR = "name"; //$NON-NLS-1$
+ public static final String PACKAGE_ATTR = "package"; //$NON-NLS-1$
+
+ /** The {@link ElementDescriptor} for the root Manifest element. */
+ private final ElementDescriptor MANIFEST_ELEMENT;
+ /** The {@link ElementDescriptor} for the root Application element. */
+ private final ElementDescriptor APPLICATION_ELEMENT;
+
+ /** The {@link ElementDescriptor} for the root Instrumentation element. */
+ private final ElementDescriptor INTRUMENTATION_ELEMENT;
+ /** The {@link ElementDescriptor} for the root Permission element. */
+ private final ElementDescriptor PERMISSION_ELEMENT;
+ /** The {@link ElementDescriptor} for the root UsesPermission element. */
+ private final ElementDescriptor USES_PERMISSION_ELEMENT;
+ /** The {@link ElementDescriptor} for the root UsesSdk element. */
+ private final ElementDescriptor USES_SDK_ELEMENT;
+
+ /** The {@link ElementDescriptor} for the root PermissionGroup element. */
+ private final ElementDescriptor PERMISSION_GROUP_ELEMENT;
+ /** The {@link ElementDescriptor} for the root PermissionTree element. */
+ private final ElementDescriptor PERMISSION_TREE_ELEMENT;
+
+ /** Private package attribute for the manifest element. Needs to be handled manually. */
+ private final TextAttributeDescriptor PACKAGE_ATTR_DESC;
+
+ public AndroidManifestDescriptors() {
+ APPLICATION_ELEMENT = createElement("application", null, Mandatory.MANDATORY_LAST); //$NON-NLS-1$ + no child & mandatory
+ INTRUMENTATION_ELEMENT = createElement("instrumentation"); //$NON-NLS-1$
+
+ PERMISSION_ELEMENT = createElement("permission"); //$NON-NLS-1$
+ USES_PERMISSION_ELEMENT = createElement(USES_PERMISSION);
+ USES_SDK_ELEMENT = createElement("uses-sdk", null, Mandatory.MANDATORY); //$NON-NLS-1$ + no child & mandatory
+
+ PERMISSION_GROUP_ELEMENT = createElement("permission-group"); //$NON-NLS-1$
+ PERMISSION_TREE_ELEMENT = createElement("permission-tree"); //$NON-NLS-1$
+
+ MANIFEST_ELEMENT = createElement(
+ MANIFEST_NODE_NAME, // xml name
+ new ElementDescriptor[] {
+ APPLICATION_ELEMENT,
+ INTRUMENTATION_ELEMENT,
+ PERMISSION_ELEMENT,
+ USES_PERMISSION_ELEMENT,
+ PERMISSION_GROUP_ELEMENT,
+ PERMISSION_TREE_ELEMENT,
+ USES_SDK_ELEMENT,
+ },
+ Mandatory.MANDATORY);
+
+ // The "package" attribute is treated differently as it doesn't have the standard
+ // Android XML namespace.
+ PACKAGE_ATTR_DESC = new PackageAttributeDescriptor(PACKAGE_ATTR,
+ null /* nsUri */,
+ new AttributeInfo(PACKAGE_ATTR, Format.REFERENCE_SET)).setTooltip(
+ "This attribute gives a unique name for the package, using a Java-style " +
+ "naming convention to avoid name collisions.\nFor example, applications " +
+ "published by Google could have names of the form com.google.app.appname");
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return new ElementDescriptor[] { MANIFEST_ELEMENT };
+ }
+
+ @Override
+ public ElementDescriptor getDescriptor() {
+ return getManifestElement();
+ }
+
+ public ElementDescriptor getApplicationElement() {
+ return APPLICATION_ELEMENT;
+ }
+
+ public ElementDescriptor getManifestElement() {
+ return MANIFEST_ELEMENT;
+ }
+
+ public ElementDescriptor getUsesSdkElement() {
+ return USES_SDK_ELEMENT;
+ }
+
+ public ElementDescriptor getInstrumentationElement() {
+ return INTRUMENTATION_ELEMENT;
+ }
+
+ public ElementDescriptor getPermissionElement() {
+ return PERMISSION_ELEMENT;
+ }
+
+ public ElementDescriptor getUsesPermissionElement() {
+ return USES_PERMISSION_ELEMENT;
+ }
+
+ public ElementDescriptor getPermissionGroupElement() {
+ return PERMISSION_GROUP_ELEMENT;
+ }
+
+ public ElementDescriptor getPermissionTreeElement() {
+ return PERMISSION_TREE_ELEMENT;
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then updates them
+ * all at once.
+ *
+ * @param manifestMap The map style => attributes from the attrs_manifest.xml file
+ */
+ public synchronized void updateDescriptors(
+ Map<String, DeclareStyleableInfo> manifestMap) {
+
+ // -- setup the required attributes overrides --
+
+ Set<String> required = new HashSet<String>();
+ required.add("provider/authorities"); //$NON-NLS-1$
+
+ // -- setup the various attribute format overrides --
+
+ // The key for each override is "element1,element2,.../attr-xml-local-name" or
+ // "*/attr-xml-local-name" to match the attribute in any element.
+
+ Map<String, ITextAttributeCreator> overrides = new HashMap<String, ITextAttributeCreator>();
+
+ overrides.put("*/icon", ReferenceAttributeDescriptor.CREATOR); //$NON-NLS-1$
+
+ overrides.put("*/theme", ThemeAttributeDescriptor.CREATOR); //$NON-NLS-1$
+ overrides.put("*/permission", ListAttributeDescriptor.CREATOR); //$NON-NLS-1$
+ overrides.put("*/targetPackage", ManifestPkgAttrDescriptor.CREATOR); //$NON-NLS-1$
+
+ overrides.put("uses-library/name", ListAttributeDescriptor.CREATOR); //$NON-NLS-1$
+ overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR, //$NON-NLS-1$
+ ListAttributeDescriptor.CREATOR);
+
+ overrideClassName(overrides, "application", //$NON-NLS-1$
+ SdkConstants.CLASS_APPLICATION,
+ false /*mandatory*/);
+ overrideClassName(overrides, "application/backupAgent", //$NON-NLS-1$
+ "android.app.backup.BackupAgent", //$NON-NLS-1$
+ false /*mandatory*/);
+ overrideClassName(overrides, "activity", SdkConstants.CLASS_ACTIVITY); //$NON-NLS-1$
+ overrideClassName(overrides, "receiver", SdkConstants.CLASS_BROADCASTRECEIVER);//$NON-NLS-1$
+ overrideClassName(overrides, "service", SdkConstants.CLASS_SERVICE); //$NON-NLS-1$
+ overrideClassName(overrides, "provider", SdkConstants.CLASS_CONTENTPROVIDER); //$NON-NLS-1$
+ overrideClassName(overrides, "instrumentation",
+ SdkConstants.CLASS_INSTRUMENTATION); //$NON-NLS-1$
+
+ // -- list element nodes already created --
+ // These elements are referenced by already opened editors, so we want to update them
+ // but not re-create them when reloading an SDK on the fly.
+
+ HashMap<String, ElementDescriptor> elementDescs =
+ new HashMap<String, ElementDescriptor>();
+ elementDescs.put(MANIFEST_ELEMENT.getXmlLocalName(), MANIFEST_ELEMENT);
+ elementDescs.put(APPLICATION_ELEMENT.getXmlLocalName(), APPLICATION_ELEMENT);
+ elementDescs.put(INTRUMENTATION_ELEMENT.getXmlLocalName(), INTRUMENTATION_ELEMENT);
+ elementDescs.put(PERMISSION_ELEMENT.getXmlLocalName(), PERMISSION_ELEMENT);
+ elementDescs.put(USES_PERMISSION_ELEMENT.getXmlLocalName(), USES_PERMISSION_ELEMENT);
+ elementDescs.put(USES_SDK_ELEMENT.getXmlLocalName(), USES_SDK_ELEMENT);
+ elementDescs.put(PERMISSION_GROUP_ELEMENT.getXmlLocalName(), PERMISSION_GROUP_ELEMENT);
+ elementDescs.put(PERMISSION_TREE_ELEMENT.getXmlLocalName(), PERMISSION_TREE_ELEMENT);
+
+ // --
+
+ inflateElement(manifestMap,
+ overrides,
+ required,
+ elementDescs,
+ MANIFEST_ELEMENT,
+ "AndroidManifest"); //$NON-NLS-1$
+ insertAttribute(MANIFEST_ELEMENT, PACKAGE_ATTR_DESC);
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(
+ SdkConstants.ANDROID_NS_NAME, SdkConstants.ANDROID_URI);
+ insertAttribute(MANIFEST_ELEMENT, xmlns);
+
+ /*
+ *
+ *
+ */
+ assert sanityCheck(manifestMap, MANIFEST_ELEMENT);
+ }
+
+ /**
+ * Sets up a mandatory attribute override using a ClassAttributeDescriptor
+ * with the specified class name.
+ *
+ * @param overrides The current map of overrides.
+ * @param elementName The element name to override, e.g. "application".
+ * If this name does NOT have a slash (/), the ANDROID_NAME_ATTR attribute will be overriden.
+ * Otherwise, if it contains a (/) the format is "element/attribute", for example
+ * "application/name" vs "application/backupAgent".
+ * @param className The fully qualified name of the base class of the attribute.
+ */
+ private static void overrideClassName(
+ Map<String, ITextAttributeCreator> overrides,
+ String elementName,
+ final String className) {
+ overrideClassName(overrides, elementName, className, true);
+ }
+
+ /**
+ * Sets up an attribute override using a ClassAttributeDescriptor
+ * with the specified class name.
+ *
+ * @param overrides The current map of overrides.
+ * @param elementName The element name to override, e.g. "application".
+ * If this name does NOT have a slash (/), the ANDROID_NAME_ATTR attribute will be overriden.
+ * Otherwise, if it contains a (/) the format is "element/attribute", for example
+ * "application/name" vs "application/backupAgent".
+ * @param className The fully qualified name of the base class of the attribute.
+ * @param mandatory True if this attribute is mandatory, false if optional.
+ */
+ private static void overrideClassName(
+ Map<String, ITextAttributeCreator> overrides,
+ String elementName,
+ final String className,
+ final boolean mandatory) {
+ if (elementName.indexOf('/') == -1) {
+ elementName = elementName + '/' + ANDROID_NAME_ATTR;
+ }
+ overrides.put(elementName,
+ new ITextAttributeCreator() {
+ @Override
+ public TextAttributeDescriptor create(String xmlName, String nsUri,
+ IAttributeInfo attrInfo) {
+ if (attrInfo == null) {
+ attrInfo = new AttributeInfo(xmlName, Format.STRING_SET );
+ }
+
+ if (SdkConstants.CLASS_ACTIVITY.equals(className)) {
+ return new ClassAttributeDescriptor(
+ className,
+ PostActivityCreationAction.getAction(),
+ xmlName,
+ nsUri,
+ attrInfo,
+ mandatory,
+ true /*defaultToProjectOnly*/);
+ } else if (SdkConstants.CLASS_BROADCASTRECEIVER.equals(className)) {
+ return new ClassAttributeDescriptor(
+ className,
+ PostReceiverCreationAction.getAction(),
+ xmlName,
+ nsUri,
+ attrInfo,
+ mandatory,
+ true /*defaultToProjectOnly*/);
+ } else if (SdkConstants.CLASS_INSTRUMENTATION.equals(className)) {
+ return new ClassAttributeDescriptor(
+ className,
+ null, // no post action
+ xmlName,
+ nsUri,
+ attrInfo,
+ mandatory,
+ false /*defaultToProjectOnly*/);
+ } else {
+ return new ClassAttributeDescriptor(
+ className,
+ xmlName,
+ nsUri,
+ attrInfo,
+ mandatory);
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from the information given here
+ * and the javadoc & attributes extracted from the style map if any.
+ * <p/>
+ * Creates an element with no attribute overrides.
+ */
+ private ElementDescriptor createElement(
+ String xmlName,
+ ElementDescriptor[] childrenElements,
+ Mandatory mandatory) {
+ // Creates an element with no attribute overrides.
+ String styleName = guessStyleName(xmlName);
+ String sdkUrl = DescriptorsUtils.MANIFEST_SDK_URL + styleName;
+ String uiName = getUiName(xmlName);
+
+ ElementDescriptor element = new ManifestElementDescriptor(xmlName, uiName, null, sdkUrl,
+ null, childrenElements, mandatory);
+
+ return element;
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from its XML local name.
+ * <p/>
+ * This version creates an element not mandatory.
+ */
+ private ElementDescriptor createElement(String xmlName) {
+ // Creates an element with no child and not mandatory
+ return createElement(xmlName, null, Mandatory.NOT_MANDATORY);
+ }
+
+ /**
+ * Inserts an attribute in this element attribute list if it is not present there yet
+ * (based on the attribute XML name.)
+ * The attribute is inserted at the beginning of the attribute list.
+ */
+ private void insertAttribute(ElementDescriptor element, AttributeDescriptor newAttr) {
+ AttributeDescriptor[] attributes = element.getAttributes();
+ for (AttributeDescriptor attr : attributes) {
+ if (attr.getXmlLocalName().equals(newAttr.getXmlLocalName())) {
+ return;
+ }
+ }
+
+ AttributeDescriptor[] newArray = new AttributeDescriptor[attributes.length + 1];
+ newArray[0] = newAttr;
+ System.arraycopy(attributes, 0, newArray, 1, attributes.length);
+ element.setAttributes(newArray);
+ }
+
+ /**
+ * "Inflates" the properties of an {@link ElementDescriptor} from the styleable declaration.
+ * <p/>
+ * This first creates all the attributes for the given ElementDescriptor.
+ * It then finds all children of the descriptor, inflates them recursively and sets them
+ * as child to this ElementDescriptor.
+ *
+ * @param styleMap The input styleable map for manifest elements & attributes.
+ * @param overrides A list of attribute overrides (to customize the type of the attribute
+ * descriptors).
+ * @param requiredAttributes Set of attributes to be marked as required.
+ * @param existingElementDescs A map of already created element descriptors, keyed by
+ * XML local name. This is used to use the static elements created initially by this
+ * class, which are referenced directly by editors (so that reloading an SDK won't
+ * break these references).
+ * @param elemDesc The current {@link ElementDescriptor} to inflate.
+ * @param styleName The name of the {@link ElementDescriptor} to inflate. Its XML local name
+ * will be guessed automatically from the style name.
+ */
+ private void inflateElement(
+ Map<String, DeclareStyleableInfo> styleMap,
+ Map<String, ITextAttributeCreator> overrides,
+ Set<String> requiredAttributes,
+ HashMap<String, ElementDescriptor> existingElementDescs,
+ ElementDescriptor elemDesc,
+ String styleName) {
+ assert elemDesc != null;
+ assert styleName != null;
+ assert styleMap != null;
+
+ if (styleMap == null) {
+ return;
+ }
+
+ // define attributes
+ DeclareStyleableInfo style = styleMap.get(styleName);
+ if (style != null) {
+ ArrayList<AttributeDescriptor> attrDescs = new ArrayList<AttributeDescriptor>();
+ DescriptorsUtils.appendAttributes(attrDescs,
+ elemDesc.getXmlLocalName(),
+ SdkConstants.NS_RESOURCES,
+ style.getAttributes(),
+ requiredAttributes,
+ overrides);
+ elemDesc.setTooltip(style.getJavaDoc());
+ elemDesc.setAttributes(attrDescs.toArray(new AttributeDescriptor[attrDescs.size()]));
+ }
+
+ // find all elements that have this one as parent
+ ArrayList<ElementDescriptor> children = new ArrayList<ElementDescriptor>();
+ for (Entry<String, DeclareStyleableInfo> entry : styleMap.entrySet()) {
+ DeclareStyleableInfo childStyle = entry.getValue();
+ boolean isParent = false;
+ String[] parents = childStyle.getParents();
+ if (parents != null) {
+ for (String parent: parents) {
+ if (styleName.equals(parent)) {
+ isParent = true;
+ break;
+ }
+ }
+ }
+ if (isParent) {
+ String childStyleName = entry.getKey();
+ String childXmlName = guessXmlName(childStyleName);
+
+ // create or re-use element
+ ElementDescriptor child = existingElementDescs.get(childXmlName);
+ if (child == null) {
+ child = createElement(childXmlName);
+ existingElementDescs.put(childXmlName, child);
+ }
+ children.add(child);
+
+ inflateElement(styleMap,
+ overrides,
+ requiredAttributes,
+ existingElementDescs,
+ child,
+ childStyleName);
+ }
+ }
+ elemDesc.setChildren(children.toArray(new ElementDescriptor[children.size()]));
+ }
+
+ /**
+ * Get an UI name from the element XML name.
+ * <p/>
+ * Capitalizes the first letter and replace non-alphabet by a space followed by a capital.
+ */
+ private static String getUiName(String xmlName) {
+ StringBuilder sb = new StringBuilder();
+
+ boolean capitalize = true;
+ for (char c : xmlName.toCharArray()) {
+ if (capitalize && c >= 'a' && c <= 'z') {
+ sb.append((char)(c + 'A' - 'a'));
+ capitalize = false;
+ } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
+ sb.append(' ');
+ capitalize = true;
+ } else {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Guesses the style name for a given XML element name.
+ * <p/>
+ * The rules are:
+ * - capitalize the first letter:
+ * - if there's a dash, skip it and capitalize the next one
+ * - prefix AndroidManifest
+ * The exception is "manifest" which just becomes AndroidManifest.
+ * <p/>
+ * Examples:
+ * - manifest => AndroidManifest
+ * - application => AndroidManifestApplication
+ * - uses-permission => AndroidManifestUsesPermission
+ */
+ private String guessStyleName(String xmlName) {
+ StringBuilder sb = new StringBuilder();
+
+ if (!xmlName.equals(MANIFEST_NODE_NAME)) {
+ boolean capitalize = true;
+ for (char c : xmlName.toCharArray()) {
+ if (capitalize && c >= 'a' && c <= 'z') {
+ sb.append((char)(c + 'A' - 'a'));
+ capitalize = false;
+ } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
+ // not a letter -- skip the character and capitalize the next one
+ capitalize = true;
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+
+ sb.insert(0, ANDROID_MANIFEST_STYLEABLE);
+ return sb.toString();
+ }
+
+ /**
+ * This method performs a sanity check to make sure all the styles declared in the
+ * manifestMap are actually defined in the actual element descriptors and reachable from
+ * the manifestElement root node.
+ */
+ private boolean sanityCheck(Map<String, DeclareStyleableInfo> manifestMap,
+ ElementDescriptor manifestElement) {
+ TreeSet<String> elementsDeclared = new TreeSet<String>();
+ findAllElementNames(manifestElement, elementsDeclared);
+
+ TreeSet<String> stylesDeclared = new TreeSet<String>();
+ for (String styleName : manifestMap.keySet()) {
+ if (styleName.startsWith(ANDROID_MANIFEST_STYLEABLE)) {
+ stylesDeclared.add(styleName);
+ }
+ }
+
+ for (Iterator<String> it = elementsDeclared.iterator(); it.hasNext();) {
+ String xmlName = it.next();
+ String styleName = guessStyleName(xmlName);
+ if (stylesDeclared.remove(styleName)) {
+ it.remove();
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (!stylesDeclared.isEmpty()) {
+ sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by the SDK but unknown to ADT: ");
+ for (String name : stylesDeclared) {
+ sb.append(guessXmlName(name));
+
+ if (!name.equals(stylesDeclared.last())) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ }
+
+ AdtPlugin.log(IStatus.WARNING, "%s", sb.toString());
+ AdtPlugin.printToConsole((String)null, sb);
+ sb.setLength(0);
+ }
+
+ if (!elementsDeclared.isEmpty()) {
+ sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by ADT but not by the SDK: ");
+ for (String name : elementsDeclared) {
+ sb.append(name);
+ if (!name.equals(elementsDeclared.last())) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ }
+
+ AdtPlugin.log(IStatus.WARNING, "%s", sb.toString());
+ AdtPlugin.printToConsole((String)null, sb);
+ }
+
+ return true;
+ }
+
+ /**
+ * Performs an approximate translation of the style name into a potential
+ * xml name. This is more or less the reverse from guessStyleName().
+ *
+ * @return The XML local name for a given style name.
+ */
+ private String guessXmlName(String name) {
+ StringBuilder sb = new StringBuilder();
+ if (ANDROID_MANIFEST_STYLEABLE.equals(name)) {
+ sb.append(MANIFEST_NODE_NAME);
+ } else {
+ name = name.replace(ANDROID_MANIFEST_STYLEABLE, ""); //$NON-NLS-1$
+ boolean first_char = true;
+ for (char c : name.toCharArray()) {
+ if (c >= 'A' && c <= 'Z') {
+ if (!first_char) {
+ sb.append('-');
+ }
+ c = (char) (c - 'A' + 'a');
+ }
+ sb.append(c);
+ first_char = false;
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method used by {@link #sanityCheck(Map, ElementDescriptor)} to find all the
+ * {@link ElementDescriptor} names defined by the tree of descriptors.
+ * <p/>
+ * Note: this assumes no circular reference in the tree of {@link ElementDescriptor}s.
+ */
+ private void findAllElementNames(ElementDescriptor element, TreeSet<String> declared) {
+ declared.add(element.getXmlName());
+ for (ElementDescriptor desc : element.getChildren()) {
+ findAllElementNames(desc, declared);
+ }
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java
new file mode 100644
index 000000000..1a27f8d00
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+/**
+ * Describes an XML attribute representing a class name.
+ * It is displayed by a {@link UiClassAttributeNode}.
+ */
+public class ClassAttributeDescriptor extends TextAttributeDescriptor {
+
+ /** Superclass of the class value. */
+ private String mSuperClassName;
+
+ private IPostTypeCreationAction mPostCreationAction;
+
+ /** indicates if the class parameter is mandatory */
+ boolean mMandatory;
+
+ private final boolean mDefaultToProjectOnly;
+
+ /**
+ * Creates a new {@link ClassAttributeDescriptor}
+ * @param superClassName the fully qualified name of the superclass of the class represented
+ * by the attribute.
+ * @param xmlLocalName The XML name of the attribute (case sensitive, with android: prefix).
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null.
+ * @param mandatory indicates if the class attribute is mandatory.
+ */
+ public ClassAttributeDescriptor(String superClassName,
+ String xmlLocalName,
+ String nsUri,
+ IAttributeInfo attrInfo,
+ boolean mandatory) {
+ super(xmlLocalName, nsUri, attrInfo);
+ mSuperClassName = superClassName;
+ mDefaultToProjectOnly = true;
+ if (mandatory) {
+ mMandatory = true;
+ setRequired(true);
+ }
+ }
+
+ /**
+ * Creates a new {@link ClassAttributeDescriptor}
+ * @param superClassName the fully qualified name of the superclass of the class represented
+ * by the attribute.
+ * @param postCreationAction the {@link IPostTypeCreationAction} to be executed on the
+ * newly created class.
+ * @param xmlLocalName The XML local name of the attribute (case sensitive).
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null.
+ * @param mandatory indicates if the class attribute is mandatory.
+ * @param defaultToProjectOnly True if only classes from the sources of this project should
+ * be shown by default in the class browser.
+ */
+ public ClassAttributeDescriptor(String superClassName,
+ IPostTypeCreationAction postCreationAction,
+ String xmlLocalName,
+ String nsUri,
+ IAttributeInfo attrInfo,
+ boolean mandatory,
+ boolean defaultToProjectOnly) {
+ super(xmlLocalName, nsUri, attrInfo);
+ mSuperClassName = superClassName;
+ mPostCreationAction = postCreationAction;
+ mDefaultToProjectOnly = defaultToProjectOnly;
+ if (mandatory) {
+ mMandatory = true;
+ setRequired(true);
+ }
+ }
+
+ /**
+ * @return A new {@link UiClassAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiClassAttributeNode(mSuperClassName, mPostCreationAction,
+ mMandatory, this, uiParent, mDefaultToProjectOnly);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java
new file mode 100644
index 000000000..deb815e57
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiManifestElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+/**
+ * {@link ManifestElementDescriptor} describes an XML element node, with its
+ * element name, its possible attributes, its possible child elements but also
+ * its display name and tooltip.
+ *
+ * This {@link ElementDescriptor} is specialized to create {@link UiManifestElementNode} UI nodes.
+ */
+public class ManifestElementDescriptor extends ElementDescriptor {
+
+ /**
+ * Constructs a new {@link ManifestElementDescriptor}.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models).
+ */
+ public ManifestElementDescriptor(String xml_name,
+ String ui_name,
+ String tooltip,
+ String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children,
+ Mandatory mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ }
+
+ /**
+ * Constructs a new {@link ManifestElementDescriptor}.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models).
+ */
+ public ManifestElementDescriptor(String xml_name,
+ String ui_name,
+ String tooltip,
+ String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children,
+ boolean mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ }
+
+ /**
+ * Constructs a new {@link ManifestElementDescriptor}.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public ManifestElementDescriptor(String xml_name,
+ String ui_name,
+ String tooltip,
+ String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, false);
+ }
+
+ /**
+ * This is a shortcut for
+ * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, children).
+ * This constructor is mostly used for unit tests.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ */
+ public ManifestElementDescriptor(String xml_name, ElementDescriptor[] children) {
+ super(xml_name, children);
+ }
+
+ /**
+ * This is a shortcut for
+ * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, null).
+ * This constructor is mostly used for unit tests.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ */
+ public ManifestElementDescriptor(String xml_name) {
+ super(xml_name, null);
+ }
+
+ /**
+ * @return A new {@link UiElementNode} linked to this descriptor.
+ */
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiManifestElementNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java
new file mode 100755
index 000000000..74b789487
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiManifestPkgAttrNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+/**
+ * Describes a package XML attribute. It is displayed by a {@link UiManifestPkgAttrNode}.
+ * <p/>
+ * Used by the override for .../targetPackage in {@link AndroidManifestDescriptors}.
+ */
+public class ManifestPkgAttrDescriptor extends TextAttributeDescriptor {
+
+ /**
+ * Used by {@link DescriptorsUtils} to create instances of this descriptor.
+ */
+ public static final ITextAttributeCreator CREATOR = new ITextAttributeCreator() {
+ @Override
+ public TextAttributeDescriptor create(String xmlLocalName,
+ String nsUri, IAttributeInfo attrInfo) {
+ return new ManifestPkgAttrDescriptor(xmlLocalName, nsUri, attrInfo);
+ }
+ };
+
+ public ManifestPkgAttrDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ }
+
+ /**
+ * @return A new {@link UiManifestPkgAttrNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiManifestPkgAttrNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java
new file mode 100644
index 000000000..e8395ac40
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiPackageAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+/**
+ * Describes a package XML attribute. It is displayed by a {@link UiPackageAttributeNode}.
+ */
+public class PackageAttributeDescriptor extends TextAttributeDescriptor {
+
+ public PackageAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ }
+
+ /**
+ * @return A new {@link UiPackageAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiPackageAttributeNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java
new file mode 100644
index 000000000..60c663dfc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+
+/**
+ * Action to be executed after an Activity class is created.
+ */
+class PostActivityCreationAction implements IPostTypeCreationAction {
+
+ private final static PostActivityCreationAction sAction = new PostActivityCreationAction();
+
+ private PostActivityCreationAction() {
+ // private constructor to enforce singleton.
+ }
+
+
+ /**
+ * Returns the action.
+ */
+ public static IPostTypeCreationAction getAction() {
+ return sAction;
+ }
+
+ /**
+ * Processes a newly created Activity.
+ *
+ */
+ @Override
+ public void processNewType(IType newType) {
+ try {
+ String methodContent =
+ " /** Called when the activity is first created. */\n" +
+ " @Override\n" +
+ " public void onCreate(Bundle savedInstanceState) {\n" +
+ " super.onCreate(savedInstanceState);\n" +
+ "\n" +
+ " // TODO Auto-generated method stub\n" +
+ " }";
+ newType.createMethod(methodContent, null /* sibling*/, false /* force */,
+ new NullProgressMonitor());
+
+ // we need to add the import for Bundle, so we need the compilation unit.
+ // Since the type could be enclosed in other types, we loop till we find it.
+ ICompilationUnit compilationUnit = null;
+ IJavaElement element = newType;
+ do {
+ IJavaElement parentElement = element.getParent();
+ if (parentElement != null) {
+ if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) {
+ compilationUnit = (ICompilationUnit)parentElement;
+ }
+
+ element = parentElement;
+ } else {
+ break;
+ }
+ } while (compilationUnit == null);
+
+ if (compilationUnit != null) {
+ compilationUnit.createImport(SdkConstants.CLASS_BUNDLE,
+ null /* sibling */, new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java
new file mode 100644
index 000000000..8b2c36144
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+
+/**
+ * Action to be executed after an BroadcastReceiver class is created.
+ */
+class PostReceiverCreationAction implements IPostTypeCreationAction {
+
+ private final static PostReceiverCreationAction sAction = new PostReceiverCreationAction();
+
+ private PostReceiverCreationAction() {
+ // private constructor to enforce singleton.
+ }
+
+ /**
+ * Returns the action.
+ */
+ public static IPostTypeCreationAction getAction() {
+ return sAction;
+ }
+
+ /**
+ * Processes a newly created Activity.
+ *
+ */
+ @Override
+ public void processNewType(IType newType) {
+ try {
+ String methodContent =
+ " @Override\n" +
+ " public void onReceive(Context context, Intent intent) {\n" +
+ " // TODO Auto-generated method stub\n" +
+ " }";
+ newType.createMethod(methodContent, null /* sibling*/, false /* force */,
+ new NullProgressMonitor());
+
+ // we need to add the import for Bundle, so we need the compilation unit.
+ // Since the type could be enclosed in other types, we loop till we find it.
+ ICompilationUnit compilationUnit = null;
+ IJavaElement element = newType;
+ do {
+ IJavaElement parentElement = element.getParent();
+ if (parentElement != null) {
+ if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) {
+ compilationUnit = (ICompilationUnit)parentElement;
+ }
+
+ element = parentElement;
+ } else {
+ break;
+ }
+ } while (compilationUnit == null);
+
+ if (compilationUnit != null) {
+ compilationUnit.createImport(SdkConstants.CLASS_CONTEXT,
+ null /* sibling */, new NullProgressMonitor());
+ compilationUnit.createImport(SdkConstants.CLASS_INTENT,
+ null /* sibling */, new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ // looks like the class already existed (this happens when the user check to create
+ // inherited abstract methods).
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java
new file mode 100644
index 000000000..881d75361
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;
+
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+import com.android.resources.ResourceType;
+
+/**
+ * Describes a Theme/Style XML attribute displayed by a {@link UiResourceAttributeNode}
+ * <p/>
+ * Used by the override for .../theme in {@link AndroidManifestDescriptors}.
+ */
+public final class ThemeAttributeDescriptor extends TextAttributeDescriptor {
+
+ /**
+ * Used by {@link DescriptorsUtils} to create instances of this descriptor.
+ */
+ public static final ITextAttributeCreator CREATOR = new ITextAttributeCreator() {
+ @Override
+ public TextAttributeDescriptor create(String xmlLocalName,
+ String nsUri, IAttributeInfo attrInfo) {
+ return new ThemeAttributeDescriptor(xmlLocalName, nsUri, attrInfo);
+ }
+ };
+
+ public ThemeAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) {
+ super(xmlLocalName, nsUri, attrInfo);
+ }
+
+ /**
+ * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiResourceAttributeNode(ResourceType.STYLE, this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java
new file mode 100644
index 000000000..4c829d9ec
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java
@@ -0,0 +1,736 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.model;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.xml.AndroidManifest;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.ui.IJavaElementSearchConstants;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
+import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent;
+import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
+import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Represents an XML attribute for a class, that can be modified using a simple text field or
+ * a dialog to choose an existing class. Also, there's a link to create a new class.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiClassAttributeNode extends UiTextAttributeNode {
+
+ private String mReferenceClass;
+ private IPostTypeCreationAction mPostCreationAction;
+ private boolean mMandatory;
+ private final boolean mDefaultToProjectOnly;
+
+ private class HierarchyTypeSelection extends TypeSelectionExtension {
+
+ private IJavaProject mJavaProject;
+ private IType mReferenceType;
+ private Button mProjectOnly;
+ private boolean mUseProjectOnly;
+
+ public HierarchyTypeSelection(IProject project, String referenceClass)
+ throws JavaModelException {
+ mJavaProject = JavaCore.create(project);
+ mReferenceType = mJavaProject.findType(referenceClass);
+ }
+
+ @Override
+ public ITypeInfoFilterExtension getFilterExtension() {
+ return new ITypeInfoFilterExtension() {
+ @Override
+ public boolean select(ITypeInfoRequestor typeInfoRequestor) {
+
+ boolean projectOnly = mUseProjectOnly;
+
+ String packageName = typeInfoRequestor.getPackageName();
+ String typeName = typeInfoRequestor.getTypeName();
+ String enclosingType = typeInfoRequestor.getEnclosingName();
+
+ // build the full class name.
+ StringBuilder sb = new StringBuilder(packageName);
+ sb.append('.');
+ if (enclosingType.length() > 0) {
+ sb.append(enclosingType);
+ sb.append('.');
+ }
+ sb.append(typeName);
+
+ String className = sb.toString();
+
+ try {
+ IType type = mJavaProject.findType(className);
+
+ if (type == null) {
+ return false;
+ }
+
+ // don't display abstract classes
+ if ((type.getFlags() & Flags.AccAbstract) != 0) {
+ return false;
+ }
+
+ // if project-only is selected, make sure the package fragment is
+ // an actual source (thus "from this project").
+ if (projectOnly) {
+ IPackageFragment frag = type.getPackageFragment();
+ if (frag == null || frag.getKind() != IPackageFragmentRoot.K_SOURCE) {
+ return false;
+ }
+ }
+
+ // get the type hierarchy and reference type is one of the super classes.
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
+ new NullProgressMonitor());
+
+ IType[] supertypes = hierarchy.getAllSupertypes(type);
+ int n = supertypes.length;
+ for (int i = 0; i < n; i++) {
+ IType st = supertypes[i];
+ if (mReferenceType.equals(st)) {
+ return true;
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public Control createContentArea(Composite parent) {
+
+ mProjectOnly = new Button(parent, SWT.CHECK);
+ mProjectOnly.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mProjectOnly.setText(String.format("Display classes from sources of project '%s' only",
+ mJavaProject.getProject().getName()));
+
+ mUseProjectOnly = mDefaultToProjectOnly;
+ mProjectOnly.setSelection(mUseProjectOnly);
+
+ mProjectOnly.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ mUseProjectOnly = mProjectOnly.getSelection();
+ getTypeSelectionComponent().triggerSearch();
+ }
+ });
+
+ return super.createContentArea(parent);
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide a method processing newly created classes.
+ */
+ public static interface IPostTypeCreationAction {
+ /**
+ * Sent to process a newly created class.
+ * @param newType the IType representing the newly created class.
+ */
+ public void processNewType(IType newType);
+ }
+
+ /**
+ * Creates a {@link UiClassAttributeNode} object that will display ui to select or create
+ * classes.
+ * @param referenceClass The allowed supertype of the classes that are to be selected
+ * or created. Can be null.
+ * @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation
+ * modification of the class.
+ * @param mandatory indicates if the class value is mandatory
+ * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
+ * @param defaultToProjectOnly When true display classes of this project only by default.
+ * When false any class path will be considered. The user can always toggle this.
+ */
+ public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction,
+ boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent,
+ boolean defaultToProjectOnly) {
+ super(attributeDescriptor, uiParent);
+
+ mReferenceClass = referenceClass;
+ mPostCreationAction = postCreationAction;
+ mMandatory = mandatory;
+ mDefaultToProjectOnly = defaultToProjectOnly;
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ StringBuilder label = new StringBuilder();
+ label.append("<form><p><a href='unused'>");
+ label.append(desc.getUiName());
+ label.append("</a></p></form>");
+ FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
+ label.toString(), true /* setupLayoutData */);
+ formText.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ handleLabelClick();
+ }
+ });
+ formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(formText, desc.getTooltip());
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ handleBrowseClick();
+ }
+ });
+ }
+
+ /* (non-java doc)
+ *
+ * Add a modify listener that will check the validity of the class
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ try {
+ String textValue = text.getText().trim();
+ if (textValue.length() == 0) {
+ if (mMandatory) {
+ setErrorMessage("Value is mandatory", text);
+ } else {
+ setErrorMessage(null, text);
+ }
+ return;
+ }
+ // first we need the current java package.
+ String javaPackage = getManifestPackage();
+
+ // build the fully qualified name of the class
+ String className = AndroidManifest.combinePackageAndClassName(
+ javaPackage, textValue);
+
+ // only test the vilibility for activities.
+ boolean testVisibility = SdkConstants.CLASS_ACTIVITY.equals(
+ mReferenceClass);
+
+ // test the class
+ setErrorMessage(BaseProjectHelper.testClassForManifest(
+ BaseProjectHelper.getJavaProject(getProject()), className,
+ mReferenceClass, testVisibility), text);
+ } catch (CoreException ce) {
+ setErrorMessage(ce.getMessage(), text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ // we don't want to use setErrorMessage, because we don't want to reset
+ // the error flag in the UiAttributeNode
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+
+ private void handleBrowseClick() {
+ Text text = getTextWidget();
+
+ // we need to get the project of the manifest.
+ IProject project = getProject();
+ if (project != null) {
+
+ // Create a search scope including only the source folder of the current
+ // project.
+ IPackageFragmentRoot[] packageFragmentRoots = getPackageFragmentRoots(project,
+ true /*include_containers*/);
+ IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
+ packageFragmentRoots,
+ false);
+
+ try {
+ SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(),
+ PlatformUI.getWorkbench().getProgressService(),
+ scope,
+ IJavaElementSearchConstants.CONSIDER_CLASSES, // style
+ false, // no multiple selection
+ "**", //$NON-NLS-1$ //filter
+ new HierarchyTypeSelection(project, mReferenceClass));
+ dlg.setMessage(String.format("Select class name for element %1$s:",
+ getUiParent().getBreadcrumbTrailDescription(false /* include_root */)));
+ if (dlg instanceof ITypeSelectionComponent) {
+ ((ITypeSelectionComponent)dlg).triggerSearch();
+ }
+
+ if (dlg.open() == Window.OK) {
+ Object[] results = dlg.getResult();
+ if (results.length == 1) {
+ handleNewType((IType)results[0]);
+ }
+ }
+ } catch (JavaModelException e1) {
+ AdtPlugin.log(e1, "UiClassAttributeNode HandleBrowser failed");
+ }
+ }
+ }
+
+ private void handleLabelClick() {
+ // get the current value
+ String className = getTextWidget().getText().trim();
+
+ // get the package name from the manifest.
+ String packageName = getManifestPackage();
+
+ if (className.length() == 0) {
+ createNewClass(packageName, null /* className */);
+ } else {
+ // build back the fully qualified class name.
+ String fullClassName = className;
+ if (className.startsWith(".")) { //$NON-NLS-1$
+ fullClassName = packageName + className;
+ } else {
+ String[] segments = className.split(AdtConstants.RE_DOT);
+ if (segments.length == 1) {
+ fullClassName = packageName + "." + className; //$NON-NLS-1$
+ }
+ }
+
+ // in case the type is enclosed, we need to replace the $ with .
+ fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$
+
+ // now we try to find the file that contains this class and we open it in the editor.
+ IProject project = getProject();
+ IJavaProject javaProject = JavaCore.create(project);
+
+ try {
+ IType result = javaProject.findType(fullClassName);
+ if (result != null) {
+ JavaUI.openInEditor(result);
+ } else {
+ // split the last segment from the fullClassname
+ int index = fullClassName.lastIndexOf('.');
+ if (index != -1) {
+ createNewClass(fullClassName.substring(0, index),
+ fullClassName.substring(index+1));
+ } else {
+ createNewClass(packageName, className);
+ }
+ }
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
+ }
+ }
+ }
+
+ private IProject getProject() {
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ IEditorInput input = editor.getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ // from the file editor we can get the IFile object, and from it, the IProject.
+ IFile file = ((IFileEditorInput)input).getFile();
+ return file.getProject();
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns the current value of the /manifest/package attribute.
+ * @return the package or an empty string if not found
+ */
+ private String getManifestPackage() {
+ // get the root uiNode to get the 'package' attribute value.
+ UiElementNode rootNode = getUiParent().getUiRoot();
+
+ Element xmlElement = (Element) rootNode.getXmlNode();
+
+ if (xmlElement != null) {
+ return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR);
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+
+ /**
+ * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of
+ * the specified project.
+ * @param project the project
+ * @param include_containers True to include containers
+ * @return an array of IPackageFragmentRoot.
+ */
+ private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
+ boolean include_containers) {
+ ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
+ try {
+ IJavaProject javaProject = JavaCore.create(project);
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+ for (int i = 0; i < roots.length; i++) {
+ IClasspathEntry entry = roots[i].getRawClasspathEntry();
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
+ (include_containers &&
+ entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
+ result.add(roots[i]);
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return result.toArray(new IPackageFragmentRoot[result.size()]);
+ }
+
+ private void handleNewType(IType type) {
+ Text text = getTextWidget();
+
+ // get the fully qualified name with $ to properly detect the enclosing types.
+ String name = type.getFullyQualifiedName('$');
+
+ String packageValue = getManifestPackage();
+
+ // check if the class doesn't start with the package.
+ if (packageValue.length() > 0 && name.startsWith(packageValue)) {
+ // if it does, we remove the package and the first dot.
+ name = name.substring(packageValue.length() + 1);
+
+ // look for how many segments we have left.
+ // if one, just write it that way.
+ // if more than one, write it with a leading dot.
+ String[] packages = name.split(AdtConstants.RE_DOT);
+ if (packages.length == 1) {
+ text.setText(name);
+ } else {
+ text.setText("." + name); //$NON-NLS-1$
+ }
+ } else {
+ text.setText(name);
+ }
+ }
+
+ private void createNewClass(String packageName, String className) {
+ // create the wizard page for the class creation, and configure it
+ NewClassWizardPage page = new NewClassWizardPage();
+
+ // set the parent class
+ page.setSuperClass(mReferenceClass, true /* canBeModified */);
+
+ // get the source folders as java elements.
+ IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject(),
+ true /*include_containers*/);
+
+ IPackageFragmentRoot currentRoot = null;
+ IPackageFragment currentFragment = null;
+ int packageMatchCount = -1;
+
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ // we have a perfect match! we use it.
+ currentRoot = root;
+ currentFragment = fragment;
+ packageMatchCount = -1;
+ break;
+ } else {
+ // we don't have a match. we look for the fragment with the best match
+ // (ie the closest parent package we can find)
+ try {
+ IJavaElement[] children;
+ children = root.getChildren();
+ for (IJavaElement child : children) {
+ if (child instanceof IPackageFragment) {
+ fragment = (IPackageFragment)child;
+ if (packageName.startsWith(fragment.getElementName())) {
+ // its a match. get the number of segments
+ String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
+ if (segments.length > packageMatchCount) {
+ packageMatchCount = segments.length;
+ currentFragment = fragment;
+ currentRoot = root;
+ }
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ // Couldn't get the children: we just ignore this package root.
+ }
+ }
+ }
+
+ ArrayList<IPackageFragment> createdFragments = null;
+
+ if (currentRoot != null) {
+ // if we have a perfect match, we set it and we're done.
+ if (packageMatchCount == -1) {
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(currentFragment, true /* canBeModified */);
+ } else {
+ // we have a partial match.
+ // create the package. We have to start with the first segment so that we
+ // know what to delete in case of a cancel.
+ try {
+ createdFragments = new ArrayList<IPackageFragment>();
+
+ int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
+ int count = 0;
+ int index = -1;
+ // skip the matching packages
+ while (count < packageMatchCount) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ }
+
+ // create the rest of the segments, except for the last one as indexOf will
+ // return -1;
+ while (count < totalCount - 1) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName.substring(0, index),
+ true /* force*/, new NullProgressMonitor()));
+ }
+
+ // create the last package
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName, true /* force*/, new NullProgressMonitor()));
+
+ // set the root and fragment in the Wizard page
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
+ true /* canBeModified */);
+ } catch (JavaModelException e) {
+ // if we can't create the packages, there's a problem. we revert to the default
+ // package
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ page.setPackageFragmentRoot(root, true /* canBeModified*/);
+ page.setPackageFragment(fragment, true /* canBeModified */);
+ break;
+ }
+ }
+ }
+ }
+ } else if (roots.length > 0) {
+ // if we haven't found a valid fragment, we set the root to the first source folder.
+ page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
+ }
+
+ // if we have a starting class name we use it
+ if (className != null) {
+ page.setTypeName(className, true /* canBeModified*/);
+ }
+
+ // create the action that will open it the wizard.
+ OpenNewClassWizardAction action = new OpenNewClassWizardAction();
+ action.setConfiguredWizardPage(page);
+ action.run();
+ IJavaElement element = action.getCreatedElement();
+
+ if (element != null) {
+ if (element.getElementType() == IJavaElement.TYPE) {
+
+ IType type = (IType)element;
+
+ if (mPostCreationAction != null) {
+ mPostCreationAction.processNewType(type);
+ }
+
+ handleNewType(type);
+ }
+ } else {
+ // lets delete the packages we created just for this.
+ // we need to start with the leaf and go up
+ if (createdFragments != null) {
+ try {
+ for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
+ createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the error messages. If message is <code>null</code>, the message is removed.
+ * @param message the message to set, or <code>null</code> to remove the current message
+ * @param textWidget the {@link Text} widget associated to the message.
+ */
+ private final void setErrorMessage(String message, Text textWidget) {
+ if (message != null) {
+ setHasError(true);
+ getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */,
+ IMessageProvider.ERROR, textWidget);
+ } else {
+ setHasError(false);
+ getManagedForm().getMessageManager().removeMessage(textWidget, textWidget);
+ }
+ }
+
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ // Compute a list of existing classes for content assist completion
+ IProject project = getProject();
+ if (project == null || mReferenceClass == null) {
+ return null;
+ }
+
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ IType type = javaProject.findType(mReferenceClass);
+ // Use sets because query sometimes repeats the same class
+ Set<String> libraryTypes = new HashSet<String>(80);
+ Set<String> localTypes = new HashSet<String>(30);
+ if (type != null) {
+ ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor());
+ IType[] allSubtypes = hierarchy.getAllSubtypes(type);
+ for (IType subType : allSubtypes) {
+ int flags = subType.getFlags();
+ if (Flags.isPublic(flags) && !Flags.isAbstract(flags)) {
+ String fqcn = subType.getFullyQualifiedName();
+ if (subType.getResource() != null) {
+ localTypes.add(fqcn);
+ } else {
+ libraryTypes.add(fqcn);
+ }
+ }
+ }
+ }
+
+ List<String> local = new ArrayList<String>(localTypes);
+ List<String> library = new ArrayList<String>(libraryTypes);
+ Collections.sort(local);
+ Collections.sort(library);
+ List<String> combined = new ArrayList<String>(local.size() + library.size());
+ combined.addAll(local);
+ combined.addAll(library);
+ return combined.toArray(new String[combined.size()]);
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java
new file mode 100644
index 000000000..0151d4d46
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.model;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.ManifestElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.w3c.dom.Element;
+
+/**
+ * Represents an XML node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * Each tree viewer used in the application page's parts needs to keep a model representing
+ * each underlying node in the tree. This interface represents the base type for such a node.
+ * <p/>
+ * Each node acts as an intermediary model between the actual XML model (the real data support)
+ * and the tree viewers or the corresponding page parts.
+ * <p/>
+ * Element nodes don't contain data per se. Their data is contained in their attributes
+ * as well as their children's attributes, see {@link UiAttributeNode}.
+ * <p/>
+ * The structure of a given {@link UiElementNode} is declared by a corresponding
+ * {@link ElementDescriptor}.
+ */
+public final class UiManifestElementNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
+ *
+ * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
+ */
+ public UiManifestElementNode(ManifestElementDescriptor elementDescriptor) {
+ super(elementDescriptor);
+ }
+
+ /**
+ * Computes a short string describing the UI node suitable for tree views.
+ * Uses the element's attribute "android:name" if present, or the "android:label" one
+ * followed by the element's name if not repeated.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ @Override
+ public String getShortDescription() {
+ AndroidTargetData target = getAndroidTarget();
+ AndroidManifestDescriptors manifestDescriptors = null;
+ if (target != null) {
+ manifestDescriptors = target.getManifestDescriptors();
+ }
+
+ String name = getDescriptor().getUiName();
+
+ if (manifestDescriptors != null &&
+ getXmlNode() != null &&
+ getXmlNode() instanceof Element &&
+ getXmlNode().hasAttributes()) {
+
+ // Application and Manifest nodes have a special treatment: they are unique nodes
+ // so we don't bother trying to differentiate their strings and we fall back to
+ // just using the UI name below.
+ ElementDescriptor desc = getDescriptor();
+ if (desc != manifestDescriptors.getManifestElement() &&
+ desc != manifestDescriptors.getApplicationElement()) {
+ Element elem = (Element) getXmlNode();
+ String attr = _Element_getAttributeNS(elem,
+ SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_NAME_ATTR);
+ if (attr == null || attr.length() == 0) {
+ attr = _Element_getAttributeNS(elem,
+ SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
+ }
+ if (attr != null && attr.length() > 0) {
+ // If the ui name is repeated in the attribute value, don't use it.
+ // Typical case is to avoid ".pkg.MyActivity (Activity)".
+ if (attr.contains(name)) {
+ return attr;
+ } else {
+ return String.format("%1$s (%2$s)", attr, name);
+ }
+ }
+ }
+ }
+
+ return String.format("%1$s", name);
+ }
+
+ /**
+ * Retrieves an attribute value by local name and namespace URI.
+ * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
+ * , applications must use the value <code>null</code> as the
+ * <code>namespaceURI</code> parameter for methods if they wish to have
+ * no namespace.
+ * <p/>
+ * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
+ * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
+ * This wrapper will return null instead.
+ *
+ * @see Element#getAttributeNS(String, String)
+ * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
+ * @return The result from {@link Element#getAttributeNS(String, String)} or or an empty string.
+ */
+ private String _Element_getAttributeNS(Element element,
+ String namespaceURI,
+ String localName) {
+ try {
+ return element.getAttributeNS(namespaceURI, localName);
+ } catch (Exception ignore) {
+ return "";
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java
new file mode 100755
index 000000000..60d9125f6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.model;
+
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.actions.NewProjectAction;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizard;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.ElementListSelectionDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.util.TreeSet;
+
+/**
+ * Represents an XML attribute to select an existing manifest package, that can be modified using
+ * a simple text field or a dialog to choose an existing package.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiManifestPkgAttrNode extends UiTextAttributeNode {
+
+ /**
+ * Creates a {@link UiManifestPkgAttrNode} object that will display ui to select or create
+ * a manifest package.
+ * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
+ */
+ public UiManifestPkgAttrNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, final IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ StringBuilder label = new StringBuilder();
+ label.append("<form><p><a href='unused'>"); //$NON-NLS-1$
+ label.append(desc.getUiName());
+ label.append("</a></p></form>"); //$NON-NLS-1$
+ FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
+ label.toString(), true /* setupLayoutData */);
+ formText.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ doLabelClick();
+ }
+ });
+ formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(formText, desc.getTooltip());
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+
+ setTextWidget(text);
+
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doBrowseClick();
+ }
+ });
+
+ }
+
+ /* (non-java doc)
+ * Adds a validator to the text field that calls managedForm.getMessageManager().
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ String package_name = text.getText();
+ if (package_name.indexOf('.') < 1) {
+ getManagedForm().getMessageManager().addMessage(text,
+ "Package name should contain at least two identifiers.",
+ null /* data */, IMessageProvider.ERROR, text);
+ } else {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+
+ /**
+ * Handles response to the Browse button by creating a Package dialog.
+ * */
+ private void doBrowseClick() {
+
+ // Display the list of AndroidManifest packages in a selection dialog
+ ElementListSelectionDialog dialog = new ElementListSelectionDialog(
+ getTextWidget().getShell(),
+ new ILabelProvider() {
+ @Override
+ public Image getImage(Object element) {
+ return null;
+ }
+
+ @Override
+ public String getText(Object element) {
+ return element.toString();
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ }
+ });
+
+ dialog.setTitle("Android Manifest Package Selection");
+ dialog.setMessage("Select the Android Manifest package to target.");
+
+ dialog.setElements(getPossibleValues(null));
+
+ // open the dialog and use the object selected if OK was clicked, or null otherwise
+ if (dialog.open() == Window.OK) {
+ String result = (String) dialog.getFirstResult();
+ if (result != null && result.length() > 0) {
+ getTextWidget().setText(result);
+ }
+ }
+ }
+
+ /**
+ * Handles response to the Label hyper link being activated.
+ */
+ private void doLabelClick() {
+ // get the current package name
+ String package_name = getTextWidget().getText().trim();
+
+ if (package_name.length() == 0) {
+ createNewProject();
+ } else {
+ displayExistingManifest(package_name);
+ }
+ }
+
+ /**
+ * When the label is clicked and there's already a package name, this method
+ * attempts to find the project matching the android package name and it attempts
+ * to open the manifest editor.
+ *
+ * @param package_name The android package name to find. Must not be null.
+ */
+ private void displayExistingManifest(String package_name) {
+
+ // Look for the first project that uses this package name
+ for (IJavaProject project : BaseProjectHelper.getAndroidProjects(null /*filter*/)) {
+ // check that there is indeed a manifest file.
+ IFile manifestFile = ProjectHelper.getManifest(project.getProject());
+ if (manifestFile == null) {
+ // no file? skip this project.
+ continue;
+ }
+
+ ManifestData manifestData = AndroidManifestHelper.parseForData(manifestFile);
+ if (manifestData == null) {
+ // skip this project.
+ continue;
+ }
+
+ if (package_name.equals(manifestData.getPackage())) {
+ // Found the project.
+
+ IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (win != null) {
+ IWorkbenchPage page = win.getActivePage();
+ if (page != null) {
+ try {
+ page.openEditor(
+ new FileEditorInput(manifestFile),
+ ManifestEditor.ID,
+ true, /* activate */
+ IWorkbenchPage.MATCH_INPUT);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e,
+ "Opening editor failed for %s", //$NON-NLS-1$
+ manifestFile.getFullPath());
+ }
+ }
+ }
+
+ // We found the project; even if we failed there's no need to keep looking.
+ return;
+ }
+ }
+ }
+
+ /**
+ * Displays the New Project Wizard to create a new project.
+ * If one is successfully created, use the Android Package name.
+ */
+ private void createNewProject() {
+
+ NewProjectAction npwAction = new NewProjectAction();
+ npwAction.run(null /*action*/);
+ if (npwAction.getDialogResult() == Dialog.OK) {
+ NewProjectWizard npw = (NewProjectWizard) npwAction.getWizard();
+ String name = npw.getPackageName();
+ if (name != null && name.length() > 0) {
+ getTextWidget().setText(name);
+ }
+ }
+ }
+
+ /**
+ * Returns all the possible android package names that could be used.
+ * The prefix is not used.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ TreeSet<String> packages = new TreeSet<String>();
+
+ for (IJavaProject project : BaseProjectHelper.getAndroidProjects(null /*filter*/)) {
+ // check that there is indeed a manifest file.
+ ManifestData manifestData = AndroidManifestHelper.parseForData(project.getProject());
+ if (manifestData == null) {
+ // skip this project.
+ continue;
+ }
+
+ packages.add(manifestData.getPackage());
+ }
+
+ return packages.toArray(new String[packages.size()]);
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java
new file mode 100644
index 000000000..e6a2007b3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.model;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.OpenNewPackageWizardAction;
+import org.eclipse.jdt.ui.actions.ShowInPackageViewAction;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+
+/**
+ * Represents an XML attribute for a package, that can be modified using a simple text field or
+ * a dialog to choose an existing package. Also, there's a link to create a new package.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiPackageAttributeNode extends UiTextAttributeNode {
+
+ /**
+ * Creates a {@link UiPackageAttributeNode} object that will display ui to select or create
+ * a package.
+ * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
+ */
+ public UiPackageAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, final IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ StringBuilder label = new StringBuilder();
+ label.append("<form><p><a href='unused'>"); //$NON-NLS-1$
+ label.append(desc.getUiName());
+ label.append("</a></p></form>"); //$NON-NLS-1$
+ FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
+ label.toString(), true /* setupLayoutData */);
+ formText.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ doLabelClick();
+ }
+ });
+ formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(formText, desc.getTooltip());
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+
+ setTextWidget(text);
+
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doBrowseClick();
+ }
+ });
+
+ }
+
+ /* (non-java doc)
+ * Adds a validator to the text field that calls managedForm.getMessageManager().
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ String package_name = text.getText();
+ if (package_name.indexOf('.') < 1) {
+ getManagedForm().getMessageManager().addMessage(text,
+ "Package name should contain at least two identifiers.",
+ null /* data */, IMessageProvider.ERROR, text);
+ } else {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+
+ /**
+ * Handles response to the Browse button by creating a Package dialog.
+ * */
+ private void doBrowseClick() {
+ Text text = getTextWidget();
+
+ // we need to get the project of the manifest.
+ IProject project = getProject();
+ if (project != null) {
+
+ try {
+ SelectionDialog dlg = JavaUI.createPackageDialog(text.getShell(),
+ JavaCore.create(project), 0);
+ dlg.setTitle("Select Android Package");
+ dlg.setMessage("Select the package for the Android project.");
+ SelectionDialog.setDefaultImage(AdtPlugin.getAndroidLogo());
+
+ if (dlg.open() == Window.OK) {
+ Object[] results = dlg.getResult();
+ if (results.length == 1) {
+ setPackageTextField((IPackageFragment)results[0]);
+ }
+ }
+ } catch (JavaModelException e1) {
+ }
+ }
+ }
+
+ /**
+ * Handles response to the Label hyper link being activated.
+ */
+ private void doLabelClick() {
+ // get the current package name
+ String package_name = getTextWidget().getText().trim();
+
+ if (package_name.length() == 0) {
+ createNewPackage();
+ } else {
+ // Try to select the package in the Package Explorer for the current
+ // project and the current editor's site.
+
+ IProject project = getProject();
+ if (project == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get project for UiPackageAttribute"); //$NON-NLS-1$
+ return;
+ }
+
+ IWorkbenchPartSite site = getUiParent().getEditor().getSite();
+ if (site == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get editor site for UiPackageAttribute"); //$NON-NLS-1$
+ return;
+ }
+
+ for (IPackageFragmentRoot root : getPackageFragmentRoots(project)) {
+ IPackageFragment fragment = root.getPackageFragment(package_name);
+ if (fragment != null && fragment.exists()) {
+ ShowInPackageViewAction action = new ShowInPackageViewAction(site);
+ action.run(fragment);
+ // This action's run() doesn't provide the status (although internally it could)
+ // so we just assume it worked.
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility method that returns the project for the current file being edited.
+ *
+ * @return The IProject for the current file being edited or null.
+ */
+ private IProject getProject() {
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ IEditorInput input = editor.getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ // from the file editor we can get the IFile object, and from it, the IProject.
+ IFile file = ((IFileEditorInput)input).getFile();
+ return file.getProject();
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method that computes and returns the list of {@link IPackageFragmentRoot}
+ * corresponding to the source folder of the specified project.
+ *
+ * @param project the project
+ * @return an array of IPackageFragmentRoot. Can be empty but not null.
+ */
+ private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project) {
+ ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
+ try {
+ IJavaProject javaProject = JavaCore.create(project);
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+ for (int i = 0; i < roots.length; i++) {
+ IClasspathEntry entry = roots[i].getRawClasspathEntry();
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+ result.add(roots[i]);
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return result.toArray(new IPackageFragmentRoot[result.size()]);
+ }
+
+ /**
+ * Utility method that sets the package's text field to the package fragment's name.
+ * */
+ private void setPackageTextField(IPackageFragment type) {
+ Text text = getTextWidget();
+
+ String name = type.getElementName();
+
+ text.setText(name);
+ }
+
+
+ /**
+ * Displays and handles a "Create Package Wizard".
+ *
+ * This is invoked by doLabelClick() when clicking on the hyperlink label with an
+ * empty package text field.
+ */
+ private void createNewPackage() {
+ OpenNewPackageWizardAction action = new OpenNewPackageWizardAction();
+
+ IProject project = getProject();
+ action.setSelection(new StructuredSelection(project));
+ action.run();
+
+ IJavaElement element = action.getCreatedElement();
+ if (element != null &&
+ element.exists() &&
+ element.getElementType() == IJavaElement.PACKAGE_FRAGMENT) {
+ setPackageTextField((IPackageFragment) element);
+ }
+ }
+
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ // TODO: compute a list of existing packages for content assist completion
+ return null;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java
new file mode 100644
index 000000000..7d3f6a89f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Application's attributes section part for Application page.
+ * <p/>
+ * This part is displayed at the top of the application page and displays all the possible
+ * attributes of an application node in the AndroidManifest (icon, class name, label, etc.)
+ */
+final class ApplicationAttributesPart extends UiElementPart {
+
+ /** Listen to changes to the UI node for <application> and updates the UI */
+ private AppNodeUpdateListener mAppNodeUpdateListener;
+ /** ManagedForm needed to create the UI controls */
+ private IManagedForm mManagedForm;
+
+ public ApplicationAttributesPart(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode applicationUiNode) {
+ super(body, toolkit, editor, applicationUiNode,
+ "Application Attributes", // section title
+ "Defines the attributes specific to the application.", // section description
+ Section.TWISTIE | Section.EXPANDED);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handle by the this part.
+ */
+ @Override
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ super.setUiElementNode(uiElementNode);
+
+ createUiAttributes(mManagedForm);
+ }
+
+ /* (non-java doc)
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ * <p/>
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The owner managed form
+ */
+ @Override
+ protected void createFormControls(final IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ setTable(createTableLayout(managedForm.getToolkit(), 4 /* numColumns */));
+
+ mAppNodeUpdateListener = new AppNodeUpdateListener();
+ getUiElementNode().addUpdateListener(mAppNodeUpdateListener);
+
+ createUiAttributes(mManagedForm);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (getUiElementNode() != null && mAppNodeUpdateListener != null) {
+ getUiElementNode().removeUpdateListener(mAppNodeUpdateListener);
+ mAppNodeUpdateListener = null;
+ }
+ }
+
+ @Override
+ protected void createUiAttributes(IManagedForm managedForm) {
+ Composite table = getTable();
+ if (table == null || managedForm == null) {
+ return;
+ }
+
+ // Remove any old UI controls
+ for (Control c : table.getChildren()) {
+ c.dispose();
+ }
+
+ UiElementNode uiElementNode = getUiElementNode();
+ AttributeDescriptor[] attr_desc_list = uiElementNode.getAttributeDescriptors();
+
+ // Display the attributes in 2 columns:
+ // attr 0 | attr 4
+ // attr 1 | attr 5
+ // attr 2 | attr 6
+ // attr 3 | attr 7
+ // that is we have to fill the grid in order 0, 4, 1, 5, 2, 6, 3, 7
+ // thus index = i/2 + (i is odd * n/2)
+ int n = attr_desc_list.length;
+ int n2 = (int) Math.ceil(n / 2.0);
+ for (int i = 0; i < n; i++) {
+ AttributeDescriptor attr_desc = attr_desc_list[i / 2 + (i & 1) * n2];
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ }
+
+ UiAttributeNode ui_attr = uiElementNode.findUiAttribute(attr_desc);
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+ } else {
+ // The XML has an extra attribute which wasn't declared in
+ // AndroidManifestDescriptors. This is not a problem, we just ignore it.
+ AdtPlugin.log(IStatus.WARNING,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ uiElementNode.getDescriptor().getXmlName());
+ }
+ }
+
+ if (n == 0) {
+ createLabel(table, managedForm.getToolkit(),
+ "No attributes to display, waiting for SDK to finish loading...",
+ null /* tooltip */ );
+ }
+
+ // Initialize the enabled/disabled state
+ if (mAppNodeUpdateListener != null) {
+ mAppNodeUpdateListener.uiElementNodeUpdated(uiElementNode, null /* state, not used */);
+ }
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * This listener synchronizes the UI with the actual presence of the application XML node.
+ */
+ private class AppNodeUpdateListener implements IUiUpdateListener {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // We enable the UI here if the XML node is not null.
+ Composite table = getTable();
+ boolean exists = (ui_node.getXmlNode() != null);
+ if (table != null && table.getEnabled() != exists) {
+ table.setEnabled(exists);
+ for (Control c : table.getChildren()) {
+ c.setEnabled(exists);
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java
new file mode 100644
index 000000000..06a3d3f3e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+
+/**
+ * Page for "Application" settings, part of the AndroidManifest form editor.
+ * <p/>
+ * Useful reference:
+ * <a href="http://www.eclipse.org/articles/Article-Forms/article.html">
+ * http://www.eclipse.org/articles/Article-Forms/article.html</a>
+ */
+public final class ApplicationPage extends FormPage implements IPageImageProvider {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "application_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+ /** The Application Toogle part */
+ private ApplicationToggle mTooglePart;
+ /** The Application Attributes part */
+ private ApplicationAttributesPart mAttrPart;
+ /** The tree view block */
+ private UiTreeBlock mTreeBlock;
+
+ public ApplicationPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Application"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon(getTitle(),
+ IconFactory.COLOR_BLUE,
+ IconFactory.SHAPE_RECT);
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Application");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode appUiNode = getUiApplicationNode();
+
+ Composite body = form.getBody();
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ // We usually prefer to have a ColumnLayout here. However
+ // MasterDetailsBlock.createContent() below will reset the body's layout to a grid layout.
+ mTooglePart = new ApplicationToggle(body, toolkit, mEditor, appUiNode);
+ mTooglePart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(mTooglePart);
+ mAttrPart = new ApplicationAttributesPart(body, toolkit, mEditor, appUiNode);
+ mAttrPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(mAttrPart);
+
+ mTreeBlock = new UiTreeBlock(mEditor, appUiNode,
+ false /* autoCreateRoot */,
+ null /* element filters */,
+ "Application Nodes",
+ "List of all elements in the application");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Retrieves the application UI node. Since this is a mandatory node, it *always*
+ * exists, even if there is no matching XML node.
+ */
+ private UiElementNode getUiApplicationNode() {
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+ if (manifestDescriptor != null) {
+ ElementDescriptor desc = manifestDescriptor.getApplicationElement();
+ return mEditor.getUiRootNode().findUiChildNode(desc.getXmlName());
+ } else {
+ // return the ui root node, as a dummy application root node.
+ return mEditor.getUiRootNode();
+ }
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiApplicationNode() {
+ UiElementNode appUiNode = getUiApplicationNode();
+ if (mTooglePart != null) {
+ mTooglePart.setUiElementNode(appUiNode);
+ }
+ if (mAttrPart != null) {
+ mAttrPart.setUiElementNode(appUiNode);
+ }
+ if (mTreeBlock != null) {
+ mTreeBlock.changeRootAndDescriptors(appUiNode,
+ null /* element filters */,
+ true /* refresh */);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java
new file mode 100644
index 000000000..159f08959
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+/**
+ * Appllication Toogle section part for application page.
+ */
+final class ApplicationToggle extends UiElementPart {
+
+ /** Checkbox indicating whether an application node is present */
+ private Button mCheckbox;
+ /** Listen to changes to the UI node for <application> and updates the checkbox */
+ private AppNodeUpdateListener mAppNodeUpdateListener;
+ /** Internal flag to know where we're programmatically modifying the checkbox and we want to
+ * avoid triggering the checkbox's callback. */
+ public boolean mInternalModification;
+ private FormText mTooltipFormText;
+
+ public ApplicationToggle(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode applicationUiNode) {
+ super(body, toolkit, editor, applicationUiNode,
+ "Application Toggle",
+ null, /* description */
+ Section.TWISTIE | Section.EXPANDED);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (getUiElementNode() != null && mAppNodeUpdateListener != null) {
+ getUiElementNode().removeUpdateListener(mAppNodeUpdateListener);
+ mAppNodeUpdateListener = null;
+ }
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handle by the this part.
+ */
+ @Override
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ super.setUiElementNode(uiElementNode);
+
+ updateTooltip();
+
+ // Set the state of the checkbox
+ mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(),
+ UiUpdateState.CHILDREN_CHANGED);
+ }
+
+ /**
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ *
+ * @param managedForm The owner managed form
+ */
+ @Override
+ protected void createFormControls(IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite table = createTableLayout(toolkit, 1 /* numColumns */);
+
+ mTooltipFormText = createFormText(table, toolkit, true, "<form></form>",
+ false /* setupLayoutData */);
+ updateTooltip();
+
+ mCheckbox = toolkit.createButton(table,
+ "Define an <application> tag in the AndroidManifest.xml",
+ SWT.CHECK);
+ mCheckbox.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
+ mCheckbox.setSelection(false);
+ mCheckbox.addSelectionListener(new CheckboxSelectionListener());
+
+ mAppNodeUpdateListener = new AppNodeUpdateListener();
+ getUiElementNode().addUpdateListener(mAppNodeUpdateListener);
+
+ // Initialize the state of the checkbox
+ mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(),
+ UiUpdateState.CHILDREN_CHANGED);
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * Updates the application tooltip in the form text.
+ * If there is no tooltip, the form text is hidden.
+ */
+ private void updateTooltip() {
+ boolean isVisible = false;
+
+ String tooltip = getUiElementNode().getDescriptor().getTooltip();
+ if (tooltip != null) {
+ tooltip = DescriptorsUtils.formatFormText(tooltip,
+ getUiElementNode().getDescriptor(),
+ Sdk.getCurrent().getDocumentationBaseUrl());
+
+ mTooltipFormText.setText(tooltip, true /* parseTags */, true /* expandURLs */);
+ mTooltipFormText.setImage(DescriptorsUtils.IMAGE_KEY, AdtPlugin.getAndroidLogo());
+ mTooltipFormText.addHyperlinkListener(getEditor().createHyperlinkListener());
+ isVisible = true;
+ }
+
+ mTooltipFormText.setVisible(isVisible);
+ }
+
+ /**
+ * This listener synchronizes the XML application node when the checkbox
+ * is changed by the user.
+ */
+ private class CheckboxSelectionListener extends SelectionAdapter {
+ private Node mUndoXmlNode;
+ private Node mUndoXmlParent;
+ private Node mUndoXmlNextNode;
+ private Node mUndoXmlNextElement;
+ private Document mUndoXmlDocument;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ if (!mInternalModification && getUiElementNode() != null) {
+ getUiElementNode().getEditor().wrapUndoEditXmlModel(
+ mCheckbox.getSelection()
+ ? "Create or restore Application node"
+ : "Remove Application node",
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mCheckbox.getSelection()) {
+ // The user wants an <application> node.
+ // Either restore a previous one
+ // or create a full new one.
+ boolean create = true;
+ if (mUndoXmlNode != null) {
+ create = !restoreApplicationNode();
+ }
+ if (create) {
+ getUiElementNode().createXmlNode();
+ }
+ } else {
+ // Users no longer wants the <application> node.
+ removeApplicationNode();
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Restore a previously "saved" application node.
+ *
+ * @return True if the node could be restored, false otherwise.
+ */
+ private boolean restoreApplicationNode() {
+ if (mUndoXmlDocument == null || mUndoXmlNode == null) {
+ return false;
+ }
+
+ // Validate node references...
+ mUndoXmlParent = validateNode(mUndoXmlDocument, mUndoXmlParent);
+ mUndoXmlNextNode = validateNode(mUndoXmlDocument, mUndoXmlNextNode);
+ mUndoXmlNextElement = validateNode(mUndoXmlDocument, mUndoXmlNextElement);
+
+ if (mUndoXmlParent == null){
+ // If the parent node doesn't exist, try to find a new manifest node.
+ // If it doesn't exist, create it.
+ mUndoXmlParent = getUiElementNode().getUiParent().prepareCommit();
+ mUndoXmlNextNode = null;
+ mUndoXmlNextElement = null;
+ }
+
+ boolean success = false;
+ if (mUndoXmlParent != null) {
+ // If the parent is still around, reuse the same node.
+
+ // Ideally we want to insert the node before what used to be its next sibling.
+ // If that's not possible, we try to insert it before its next sibling element.
+ // If that's not possible either, it will be inserted at the end of the parent's.
+ Node next = mUndoXmlNextNode;
+ if (next == null) {
+ next = mUndoXmlNextElement;
+ }
+ mUndoXmlParent.insertBefore(mUndoXmlNode, next);
+ if (next == null) {
+ Text sep = mUndoXmlDocument.createTextNode(SdkUtils.getLineSeparator());
+ mUndoXmlParent.insertBefore(sep, null); // insert separator before end tag
+ }
+ success = true;
+ }
+
+ // Remove internal references to avoid using them twice
+ mUndoXmlParent = null;
+ mUndoXmlNextNode = null;
+ mUndoXmlNextElement = null;
+ mUndoXmlNode = null;
+ mUndoXmlDocument = null;
+ return success;
+ }
+
+ /**
+ * Validates that the given xml_node is still either the root node or one of its
+ * direct descendants.
+ *
+ * @param root_node The root of the node hierarchy to examine.
+ * @param xml_node The XML node to find.
+ * @return Returns xml_node if it is, otherwise returns null.
+ */
+ private Node validateNode(Node root_node, Node xml_node) {
+ if (root_node == xml_node) {
+ return xml_node;
+ } else {
+ for (Node node = root_node.getFirstChild(); node != null;
+ node = node.getNextSibling()) {
+ if (root_node == xml_node || validateNode(node, xml_node) != null) {
+ return xml_node;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Removes the <application> node from the hierarchy.
+ * Before doing that, we try to remember where it was so that we can put it back
+ * in the same place.
+ */
+ private void removeApplicationNode() {
+ // Make sure the node actually exists...
+ Node xml_node = getUiElementNode().getXmlNode();
+ if (xml_node == null) {
+ return;
+ }
+
+ // Save its parent, next sibling and next element
+ mUndoXmlDocument = xml_node.getOwnerDocument();
+ mUndoXmlParent = xml_node.getParentNode();
+ mUndoXmlNextNode = xml_node.getNextSibling();
+ mUndoXmlNextElement = mUndoXmlNextNode;
+ while (mUndoXmlNextElement != null &&
+ mUndoXmlNextElement.getNodeType() != Node.ELEMENT_NODE) {
+ mUndoXmlNextElement = mUndoXmlNextElement.getNextSibling();
+ }
+
+ // Actually remove the node from the hierarchy and keep it here.
+ // The returned node looses its parents/siblings pointers.
+ mUndoXmlNode = getUiElementNode().deleteXmlNode();
+ }
+ }
+
+ /**
+ * This listener synchronizes the UI (i.e. the checkbox) with the
+ * actual presence of the application XML node.
+ */
+ private class AppNodeUpdateListener implements IUiUpdateListener {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // To update the checkbox to reflect the actual state, we just need
+ // to check if the XML node is null.
+ try {
+ mInternalModification = true;
+ boolean exists = ui_node.getXmlNode() != null;
+ if (mCheckbox.getSelection() != exists) {
+ mCheckbox.setSelection(exists);
+ }
+ } finally {
+ mInternalModification = false;
+ }
+
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java
new file mode 100644
index 000000000..a8bb34691
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for instrumentation settings, part of the AndroidManifest form editor.
+ */
+public final class InstrumentationPage extends FormPage implements IPageImageProvider {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "instrumentation_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+
+ private UiTreeBlock mTreeBlock;
+
+ public InstrumentationPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Instrumentation"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon(getTitle(),
+ IconFactory.COLOR_GREEN,
+ IconFactory.SHAPE_RECT);
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Instrumentation");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ ElementDescriptor[] descriptorFilters = null;
+ if (manifestDescriptor != null) {
+ descriptorFilters = new ElementDescriptor[] {
+ manifestDescriptor.getInstrumentationElement(),
+ };
+ }
+
+ mTreeBlock = new UiTreeBlock(mEditor, manifest,
+ true /* autoCreateRoot */,
+ descriptorFilters,
+ "Instrumentation",
+ "List of instrumentations defined in the manifest");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiNode() {
+ if (mTreeBlock != null) {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ mTreeBlock.changeRootAndDescriptors(manifest,
+ new ElementDescriptor[] {
+ manifestDescriptor.getInstrumentationElement()
+ },
+ true /* refresh */);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java
new file mode 100644
index 000000000..b0eb75a2d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.project.ExportHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Export section part for overview page.
+ */
+final class OverviewExportPart extends ManifestSectionPart {
+
+ private final OverviewPage mOverviewPage;
+
+ public OverviewExportPart(OverviewPage overviewPage, final Composite body, FormToolkit toolkit,
+ ManifestEditor editor) {
+ super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+ mOverviewPage = overviewPage;
+ Section section = getSection();
+ section.setText("Exporting");
+
+ final IProject project = getProject();
+ boolean isLibrary = false;
+ if (project != null) {
+ ProjectState state = Sdk.getProjectState(project);
+ if (state != null) {
+ isLibrary = state.isLibrary();
+ }
+ }
+
+ if (isLibrary) {
+ section.setDescription("Library project cannot be exported.");
+ Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+ createFormText(table, toolkit, true, "<form></form>", false /* setupLayoutData */);
+ } else {
+ section.setDescription("To export the application for distribution, you have the following options:");
+
+ Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+ StringBuffer buf = new StringBuffer();
+ buf.append("<form><li><a href=\"wizard\">"); //$NON-NLS-1$
+ buf.append("Use the Export Wizard");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(" to export and sign an APK");
+ buf.append("</li>"); //$NON-NLS-1$
+ buf.append("<li><a href=\"manual\">"); //$NON-NLS-1$
+ buf.append("Export an unsigned APK");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(" and sign it manually");
+ buf.append("</li></form>"); //$NON-NLS-1$
+
+ FormText text = createFormText(table, toolkit, true, buf.toString(),
+ false /* setupLayoutData */);
+ text.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ if (project != null) {
+ if ("manual".equals(e.data)) { //$NON-NLS-1$
+ // now we can export an unsigned apk for the project.
+ ExportHelper.exportUnsignedReleaseApk(project);
+ } else {
+ // call the export wizard
+ StructuredSelection selection = new StructuredSelection(project);
+
+ ExportWizard wizard = new ExportWizard();
+ wizard.init(PlatformUI.getWorkbench(), selection);
+ WizardDialog dialog = new WizardDialog(body.getShell(), wizard);
+ dialog.open();
+ }
+ }
+ }
+ });
+ }
+
+ layoutChanged();
+ }
+
+ /**
+ * Returns the project of the edited file.
+ */
+ private IProject getProject() {
+ IEditorInput input = mOverviewPage.mEditor.getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ IFile file = fileInput.getFile();
+ return file.getProject();
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java
new file mode 100644
index 000000000..98f2f9cc2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Generic info section part for overview page: it displays all the attributes from
+ * the manifest element.
+ */
+final class OverviewInfoPart extends UiElementPart {
+
+ private IManagedForm mManagedForm;
+
+ public OverviewInfoPart(Composite body, FormToolkit toolkit, ManifestEditor editor) {
+ super(body, toolkit, editor,
+ getManifestUiNode(editor), // uiElementNode
+ "Manifest General Attributes", // section title
+ "Defines general information about the AndroidManifest.xml", // section description
+ Section.TWISTIE | Section.EXPANDED);
+ }
+
+ /**
+ * Retrieves the UiElementNode that this part will edit. The node must exist
+ * and can't be null, by design, because it's a mandatory node.
+ */
+ private static UiElementNode getManifestUiNode(ManifestEditor editor) {
+ AndroidManifestDescriptors manifestDescriptors = editor.getManifestDescriptors();
+ if (manifestDescriptors != null) {
+ ElementDescriptor desc = manifestDescriptors.getManifestElement();
+ if (editor.getUiRootNode().getDescriptor() == desc) {
+ return editor.getUiRootNode();
+ } else {
+ return editor.getUiRootNode().findUiChildNode(desc.getXmlName());
+ }
+ }
+
+ // No manifest descriptor: we have a dummy UiRootNode, so we return that.
+ // The editor will be reloaded once we have the proper descriptors anyway.
+ return editor.getUiRootNode();
+ }
+
+ /**
+ * Overridden in order to capture the current managed form.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ protected void createFormControls(final IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ super.createFormControls(managedForm);
+ }
+
+ /**
+ * Removes any existing Attribute UI widgets and recreate them if the SDK has changed.
+ * <p/>
+ * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the
+ * SDK has changed.
+ */
+ public void onSdkChanged() {
+ setUiElementNode(getManifestUiNode(getEditor()));
+ createUiAttributes(mManagedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java
new file mode 100644
index 000000000..f8213753a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Links section part for overview page.
+ */
+final class OverviewLinksPart extends ManifestSectionPart {
+
+ private final ManifestEditor mEditor;
+ private FormText mFormText;
+
+ public OverviewLinksPart(Composite body, FormToolkit toolkit, ManifestEditor editor) {
+ super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+ mEditor = editor;
+ Section section = getSection();
+ section.setText("Links");
+ section.setDescription("The content of the Android Manifest is made up of three sections. You can also edit the XML directly.");
+
+ Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+ StringBuffer buf = new StringBuffer();
+ buf.append(String.format("<form><li style=\"image\" value=\"app_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$
+ ApplicationPage.PAGE_ID));
+ buf.append("Application");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Activities, intent filters, providers, services and receivers.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"perm_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$
+ PermissionPage.PAGE_ID));
+ buf.append("Permission");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Permissions defined and permissions used.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"inst_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$
+ InstrumentationPage.PAGE_ID));
+ buf.append("Instrumentation");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Instrumentation defined.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"srce_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$
+ ManifestEditor.TEXT_EDITOR_ID));
+ buf.append("XML Source");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Directly edit the AndroidManifest.xml file.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append("<li style=\"image\" value=\"android_img\">"); //$NON-NLS-1$
+ buf.append("<a href=\"http://code.google.com/android/devel/bblocks-manifest.html\">Documentation</a>: Documentation from the Android SDK for AndroidManifest.xml."); //$NON-NLS-1$
+ buf.append("</li>"); //$NON-NLS-1$
+ buf.append("</form>"); //$NON-NLS-1$
+
+ mFormText = createFormText(table, toolkit, true, buf.toString(),
+ false /* setupLayoutData */);
+
+ AndroidManifestDescriptors manifestDescriptor = editor.getManifestDescriptors();
+
+ Image androidLogo = AdtPlugin.getAndroidLogo();
+ mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$
+ mFormText.setImage("srce_img", IconFactory.getInstance().getIcon(AndroidXmlEditor.ICON_XML_PAGE));
+
+ if (manifestDescriptor != null) {
+ mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$
+ mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$
+ mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$
+ } else {
+ mFormText.setImage("app_img", androidLogo); //$NON-NLS-1$
+ mFormText.setImage("perm_img", androidLogo); //$NON-NLS-1$
+ mFormText.setImage("inst_img", androidLogo); //$NON-NLS-1$
+ }
+ mFormText.addHyperlinkListener(editor.createHyperlinkListener());
+ }
+
+ /**
+ * Update the UI with information from the new descriptors.
+ * <p/>At this point, this only refreshes the icons.
+ * <p/>
+ * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the
+ * SDK has changed.
+ */
+ public void onSdkChanged() {
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+ if (manifestDescriptor != null) {
+ mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$
+ mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$
+ mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$
+ }
+ }
+
+ private Image getIcon(ElementDescriptor desc) {
+ return desc.getCustomizedIcon();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java
new file mode 100644
index 000000000..7464f6a5f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+
+/**
+ * Page for overview settings, part of the AndroidManifest form editor.
+ * <p/>
+ * Useful reference:
+ * <a href="http://www.eclipse.org/articles/Article-Forms/article.html">
+ * http://www.eclipse.org/articles/Article-Forms/article.html</a>
+ */
+public final class OverviewPage extends FormPage implements IPageImageProvider {
+
+ /** Page id used for switching tabs programmatically */
+ final static String PAGE_ID = "overview_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+ /** Overview part (attributes for manifest) */
+ private OverviewInfoPart mOverviewPart;
+ /** Overview link part */
+ private OverviewLinksPart mOverviewLinkPart;
+
+ private UiTreeBlock mTreeBlock;
+
+ public OverviewPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Manifest"); // tab's label, user visible, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ Composite body = form.getBody();
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ // Usually we would set a ColumnLayout on body here. However the presence of the
+ // UiTreeBlock forces a GridLayout with one column so we comply with it.
+
+ mOverviewPart = new OverviewInfoPart(body, toolkit, mEditor);
+ mOverviewPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(mOverviewPart);
+
+ newManifestExtrasPart(managedForm);
+
+ OverviewExportPart exportPart = new OverviewExportPart(this, body, toolkit, mEditor);
+ exportPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(exportPart);
+
+ mOverviewLinkPart = new OverviewLinksPart(body, toolkit, mEditor);
+ mOverviewLinkPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(mOverviewLinkPart);
+ }
+
+ private void newManifestExtrasPart(IManagedForm managedForm) {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ mTreeBlock = new UiTreeBlock(mEditor, manifest,
+ true /* autoCreateRoot */,
+ computeManifestExtraFilters(),
+ "Manifest Extras",
+ "Extra manifest elements");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiApplicationNode() {
+ if (mOverviewPart != null) {
+ mOverviewPart.onSdkChanged();
+ }
+
+ if (mOverviewLinkPart != null) {
+ mOverviewLinkPart.onSdkChanged();
+ }
+
+ if (mTreeBlock != null) {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ mTreeBlock.changeRootAndDescriptors(manifest,
+ computeManifestExtraFilters(),
+ true /* refresh */);
+ }
+ }
+
+ private ElementDescriptor[] computeManifestExtraFilters() {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ if (manifestDescriptor == null) {
+ return null;
+ }
+
+ // get the elements we want to exclude
+ HashSet<ElementDescriptor> excludes = new HashSet<ElementDescriptor>();
+ excludes.add(manifestDescriptor.getApplicationElement());
+ excludes.add(manifestDescriptor.getInstrumentationElement());
+ excludes.add(manifestDescriptor.getPermissionElement());
+ excludes.add(manifestDescriptor.getPermissionGroupElement());
+ excludes.add(manifestDescriptor.getPermissionTreeElement());
+ excludes.add(manifestDescriptor.getUsesPermissionElement());
+
+ // walk through the known children of the manifest descriptor and keep what's not excluded
+ ArrayList<ElementDescriptor> descriptorFilters = new ArrayList<ElementDescriptor>();
+ for (ElementDescriptor child : manifest.getDescriptor().getChildren()) {
+ if (!excludes.contains(child)) {
+ descriptorFilters.add(child);
+ }
+ }
+
+ if (descriptorFilters.size() == 0) {
+ return null;
+ }
+ return descriptorFilters.toArray(new ElementDescriptor[descriptorFilters.size()]);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java
new file mode 100644
index 000000000..2f655777a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for permissions settings, part of the AndroidManifest form editor.
+ * <p/>
+ * Useful reference:
+ * <a href="http://www.eclipse.org/articles/Article-Forms/article.html">
+ * http://www.eclipse.org/articles/Article-Forms/article.html</a>
+ */
+public final class PermissionPage extends FormPage implements IPageImageProvider {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "permission_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+
+ private UiTreeBlock mTreeBlock;
+
+ public PermissionPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Permissions"); // tab label, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon(getTitle(),
+ IconFactory.COLOR_RED,
+ IconFactory.SHAPE_RECT);
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Permissions");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ ElementDescriptor[] descriptorFilters = null;
+ if (manifestDescriptor != null) {
+ descriptorFilters = new ElementDescriptor[] {
+ manifestDescriptor.getPermissionElement(),
+ manifestDescriptor.getUsesPermissionElement(),
+ manifestDescriptor.getPermissionGroupElement(),
+ manifestDescriptor.getPermissionTreeElement()
+ };
+ }
+ mTreeBlock = new UiTreeBlock(mEditor, manifest,
+ true /* autoCreateRoot */,
+ descriptorFilters,
+ "Permissions",
+ "List of permissions defined and used by the manifest");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiNode() {
+ if (mTreeBlock != null) {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ mTreeBlock.changeRootAndDescriptors(manifest,
+ new ElementDescriptor[] {
+ manifestDescriptor.getPermissionElement(),
+ manifestDescriptor.getUsesPermissionElement(),
+ manifestDescriptor.getPermissionGroupElement(),
+ manifestDescriptor.getPermissionTreeElement()
+ },
+ true /* refresh */);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuContentAssist.java
new file mode 100644
index 000000000..59068e90e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.menu;
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+/**
+ * Content Assist Processor for /res/menu XML files
+ */
+class MenuContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for LayoutContentAssist
+ */
+ public MenuContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_MENU);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuEditorDelegate.java
new file mode 100644
index 000000000..bfde86a65
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuEditorDelegate.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.menu;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceFolderType;
+import com.android.xml.AndroidXPathFactory;
+
+import org.eclipse.ui.PartInitException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Multi-page form editor for /res/menu XML files.
+ */
+public class MenuEditorDelegate extends CommonXmlDelegate {
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public MenuEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.MENU == type) {
+ return new MenuEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ AdtConstants.EDITORS_NAMESPACE + ".menu.MenuEditor"; //$NON-NLS-1$
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ private MenuEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new MenuContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ public void delegateCreateFormPages() {
+ try {
+ getEditor().addPage(new MenuTreePage(getEditor()));
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+
+ }
+
+ private boolean mUpdatingModel;
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ public void delegateXmlModelChanged(Document xml_doc) {
+ if (mUpdatingModel) {
+ return;
+ }
+
+ try {
+ mUpdatingModel = true;
+
+ // init the ui root on demand
+ delegateInitUiRootNode(false /*force*/);
+
+ getUiRootNode().setXmlDocument(xml_doc);
+ if (xml_doc != null) {
+ ElementDescriptor root_desc = getUiRootNode().getDescriptor();
+ try {
+ XPath xpath = AndroidXPathFactory.newXPath();
+ Node node = (Node) xpath.evaluate("/" + root_desc.getXmlName(), //$NON-NLS-1$
+ xml_doc,
+ XPathConstants.NODE);
+ if (node == null && root_desc.getMandatory() != Mandatory.NOT_MANDATORY) {
+ // Create the root element if it doesn't exist yet (for empty new documents)
+ node = getUiRootNode().createXmlNode();
+ }
+
+ // Refresh the manifest UI node and all its descendants
+ getUiRootNode().loadFromXmlNode(node);
+
+ // TODO ? startMonitoringMarkers();
+ } catch (XPathExpressionException e) {
+ AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$
+ root_desc.getXmlName());
+ }
+ }
+
+ } finally {
+ mUpdatingModel = false;
+ }
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiRootNode is recreated even if it already exists.
+ */
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (getUiRootNode() == null || force) {
+ Document doc = null;
+ if (getUiRootNode() != null) {
+ doc = getUiRootNode().getXmlDocument();
+ }
+
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getEditor().getTargetData();
+
+ ElementDescriptor desc;
+ if (data == null) {
+ desc = new ElementDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getMenuDescriptors().getDescriptor();
+ }
+
+ setUiRootNode(desc.createUiNode());
+ getUiRootNode().setEditor(getEditor());
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Reloads the UI manifest node from the XML, and calls the pages to update.
+ */
+ private void onDescriptorsChanged(Document document) {
+ if (document != null) {
+ getUiRootNode().loadFromXmlNode(document);
+ } else {
+ getUiRootNode().reloadFromXmlNode(getUiRootNode().getXmlNode());
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuTreePage.java
new file mode 100644
index 000000000..e24188541
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuTreePage.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.menu;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for the menu form editor.
+ */
+public final class MenuTreePage extends FormPage implements IPageImageProvider {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "layout_tree_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ CommonXmlEditor mEditor;
+
+ public MenuTreePage(CommonXmlEditor editor) {
+ super(editor, PAGE_ID, "Layout"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Menu");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode rootNode = mEditor.getUiRootNode();
+ UiTreeBlock block = new UiTreeBlock(mEditor, rootNode,
+ true /* autoCreateRoot */,
+ null /* no element filters */,
+ "Menu Elements",
+ "List of all menu elements in this XML file.");
+ block.createContent(managedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/descriptors/MenuDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/descriptors/MenuDescriptors.java
new file mode 100644
index 000000000..b7bab1bd3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/descriptors/MenuDescriptors.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.menu.descriptors;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.TAG_MENU;
+
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Complete description of the menu structure.
+ */
+public final class MenuDescriptors implements IDescriptorProvider {
+
+ /** The root element descriptor. */
+ private ElementDescriptor mDescriptor = null;
+
+ /** @return the root descriptor. */
+ @Override
+ public ElementDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mDescriptor.getChildren();
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then updates them
+ * all at once.
+ *
+ * @param styleMap The map style => attributes from the attrs.xml file
+ */
+ public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) {
+
+ // There are 3 elements: menu, item and group.
+ // The root element MUST be a menu.
+ // A top menu can contain items or group:
+ // - top groups can contain top items
+ // - top items can contain sub-menus
+ // A sub menu can contains sub items or sub groups:
+ // - sub groups can contain sub items
+ // - sub items cannot contain anything
+
+ if (mDescriptor == null) {
+ mDescriptor = createElement(styleMap,
+ TAG_MENU, // xmlName
+ "Menu", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ null, // childrenElements,
+ true /* mandatory */);
+ }
+
+ // -- sub menu can have sub_items, sub_groups but not sub_menus
+
+ ElementDescriptor sub_item = createElement(styleMap,
+ "item", // xmlName //$NON-NLS-1$
+ "Item", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ null, // childrenElements,
+ false /* mandatory */);
+
+ ElementDescriptor sub_group = createElement(styleMap,
+ "group", // xmlName //$NON-NLS-1$
+ "Group", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { sub_item }, // childrenElements,
+ false /* mandatory */);
+
+ ElementDescriptor sub_menu = createElement(styleMap,
+ TAG_MENU, // xmlName
+ "Sub-Menu", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { sub_item, sub_group }, // childrenElements,
+ true /* mandatory */);
+
+ // -- top menu can have all top groups and top items (which can have sub menus)
+
+ ElementDescriptor top_item = createElement(styleMap,
+ "item", // xmlName //$NON-NLS-1$
+ "Item", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { sub_menu }, // childrenElements,
+ false /* mandatory */);
+
+ ElementDescriptor top_group = createElement(styleMap,
+ "group", // xmlName //$NON-NLS-1$
+ "Group", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { top_item }, // childrenElements,
+ false /* mandatory */);
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(ANDROID_NS_NAME,
+ ANDROID_URI);
+
+ updateElement(mDescriptor, styleMap, "Menu", xmlns); //$NON-NLS-1$
+ mDescriptor.setChildren(new ElementDescriptor[] { top_item, top_group });
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from the information given here
+ * and the javadoc & attributes extracted from the style map if any.
+ */
+ private ElementDescriptor createElement(
+ Map<String, DeclareStyleableInfo> styleMap,
+ String xmlName, String uiName, String sdkUrl,
+ AttributeDescriptor extraAttribute,
+ ElementDescriptor[] childrenElements, boolean mandatory) {
+
+ ElementDescriptor element = new ElementDescriptor(xmlName, uiName, null, sdkUrl,
+ null, childrenElements, mandatory);
+
+ return updateElement(element, styleMap,
+ getStyleName(xmlName),
+ extraAttribute);
+ }
+
+ /**
+ * Updates an ElementDescriptor with the javadoc & attributes extracted from the style
+ * map if any.
+ */
+ private ElementDescriptor updateElement(ElementDescriptor element,
+ Map<String, DeclareStyleableInfo> styleMap,
+ String styleName,
+ AttributeDescriptor extraAttribute) {
+ ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>();
+
+ DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null;
+ if (style != null) {
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ ANDROID_URI,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ element.setTooltip(style.getJavaDoc());
+ }
+
+ if (extraAttribute != null) {
+ descs.add(extraAttribute);
+ }
+
+ element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()]));
+ return element;
+ }
+
+ /**
+ * Returns the style name (i.e. the <declare-styleable> name found in attrs.xml)
+ * for a given XML element name.
+ * <p/>
+ * The rule is that all elements have for style name:
+ * - their xml name capitalized
+ * - a "Menu" prefix, except for <menu> itself which is just "Menu".
+ */
+ private String getStyleName(String xmlName) {
+ String styleName = AdtUtils.capitalize(xmlName);
+
+ // This is NOT the UI Name but the expected internal style name
+ final String MENU_STYLE_BASE_NAME = "Menu"; //$NON-NLS-1$
+
+ if (!styleName.equals(MENU_STYLE_BASE_NAME)) {
+ styleName = MENU_STYLE_BASE_NAME + styleName;
+ }
+ return styleName;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlContentAssist.java
new file mode 100644
index 000000000..19a8d85b2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.otherxml;
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+/**
+ * Content Assist Processor for /res/xml XML files
+ */
+class OtherXmlContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for LayoutContentAssist
+ */
+ public OtherXmlContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_OTHER_XML);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlEditorDelegate.java
new file mode 100644
index 000000000..7d745165b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlEditorDelegate.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.otherxml;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.resources.ResourceFolderType;
+
+import org.eclipse.ui.PartInitException;
+import org.w3c.dom.Document;
+
+/**
+ * Multi-page form editor for /res/xml XML files.
+ */
+public class OtherXmlEditorDelegate extends CommonXmlDelegate {
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public OtherXmlEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.XML == type) {
+ return new OtherXmlEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ AdtConstants.EDITORS_NAMESPACE + ".xml.XmlEditor"; //$NON-NLS-1$
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public OtherXmlEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new OtherXmlContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ public void delegateCreateFormPages() {
+ try {
+ getEditor().addPage(new OtherXmlTreePage(getEditor()));
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+
+ }
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ public void delegateXmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ delegateInitUiRootNode(false /*force*/);
+
+ getUiRootNode().loadFromXmlNode(xml_doc);
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiRootNode is recreated even if it already exists.
+ */
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (getUiRootNode() == null || force) {
+ Document doc = null;
+ if (getUiRootNode() != null) {
+ doc = getUiRootNode().getXmlDocument();
+ }
+
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getEditor().getTargetData();
+
+ DocumentDescriptor desc;
+ if (data == null) {
+ desc = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getXmlDescriptors().getDescriptor();
+ }
+
+ setUiRootNode(desc.createUiNode());
+ getUiRootNode().setEditor(getEditor());
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Reloads the UI manifest node from the XML, and calls the pages to update.
+ */
+ private void onDescriptorsChanged(Document document) {
+ if (document != null) {
+ getUiRootNode().loadFromXmlNode(document);
+ } else {
+ getUiRootNode().reloadFromXmlNode(getUiRootNode().getXmlNode());
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlTreePage.java
new file mode 100644
index 000000000..ce81b0eb9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/OtherXmlTreePage.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.otherxml;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for the xml form editor.
+ */
+public final class OtherXmlTreePage extends FormPage implements IPageImageProvider {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "xml_tree_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ CommonXmlEditor mEditor;
+
+ public OtherXmlTreePage(CommonXmlEditor editor) {
+ super(editor, PAGE_ID, "Structure"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Xml");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode rootNode = mEditor.getUiRootNode();
+ UiTreeBlock block = new UiTreeBlock(mEditor, rootNode,
+ true /* autoCreateRoot */,
+ null /* no element filters */,
+ "Xml Elements",
+ "List of all xml elements in this XML file.");
+ block.createContent(managedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/PlainXmlEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/PlainXmlEditorDelegate.java
new file mode 100644
index 000000000..5366988f5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/PlainXmlEditorDelegate.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.otherxml;
+
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+
+import org.w3c.dom.Document;
+
+/**
+ * Plain XML editor with no form for files that have no associated descriptor data
+ */
+public class PlainXmlEditorDelegate extends CommonXmlDelegate {
+
+ /**
+ * Creates the form editor for plain XML files.
+ */
+ public PlainXmlEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new OtherXmlContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ // ---- Base Class Overrides ----
+
+ @Override
+ public void delegateCreateFormPages() {
+ }
+
+ @Override
+ public void delegateXmlModelChanged(Document xml_doc) {
+ }
+
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/descriptors/OtherXmlDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/descriptors/OtherXmlDescriptors.java
new file mode 100644
index 000000000..7f3ed0939
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/otherxml/descriptors/OtherXmlDescriptors.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.DeclareStyleableInfo;
+import com.android.ide.common.resources.platform.ViewClassInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+
+/**
+ * Description of the /res/xml structure.
+ * Currently supports the <searchable> and <preferences> root nodes.
+ */
+public final class OtherXmlDescriptors implements IDescriptorProvider {
+
+ // Public attributes names, attributes descriptors and elements descriptors referenced
+ // elsewhere.
+ public static final String PREF_KEY_ATTR = "key"; //$NON-NLS-1$
+
+ /** The root document descriptor for both searchable and preferences. */
+ private DocumentDescriptor mDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** The root document descriptor for searchable. */
+ private DocumentDescriptor mSearchDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** The root document descriptor for preferences. */
+ private DocumentDescriptor mPrefDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** The root document descriptor for widget provider. */
+ private DocumentDescriptor mAppWidgetDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** @return the root descriptor for both searchable and preferences. */
+ @Override
+ public DocumentDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mDescriptor.getChildren();
+ }
+
+ /** @return the root descriptor for searchable. */
+ public DocumentDescriptor getSearchableDescriptor() {
+ return mSearchDescriptor;
+ }
+
+ /** @return the root descriptor for preferences. */
+ public DocumentDescriptor getPreferencesDescriptor() {
+ return mPrefDescriptor;
+ }
+
+ /** @return the root descriptor for widget providers. */
+ public DocumentDescriptor getAppWidgetDescriptor() {
+ return mAppWidgetDescriptor;
+ }
+
+ public IDescriptorProvider getSearchableProvider() {
+ return new IDescriptorProvider() {
+ @Override
+ public ElementDescriptor getDescriptor() {
+ return mSearchDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mSearchDescriptor.getChildren();
+ }
+ };
+ }
+
+ public IDescriptorProvider getPreferencesProvider() {
+ return new IDescriptorProvider() {
+ @Override
+ public ElementDescriptor getDescriptor() {
+ return mPrefDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mPrefDescriptor.getChildren();
+ }
+ };
+ }
+
+ public IDescriptorProvider getAppWidgetProvider() {
+ return new IDescriptorProvider() {
+ @Override
+ public ElementDescriptor getDescriptor() {
+ return mAppWidgetDescriptor;
+ }
+
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mAppWidgetDescriptor.getChildren();
+ }
+ };
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then updates them
+ * all at once.
+ *
+ * @param searchableStyleMap The map style=>attributes for <searchable> from the attrs.xml file
+ * @param appWidgetStyleMap The map style=>attributes for <appwidget-provider> from the attrs.xml file
+ * @param prefs The list of non-group preference descriptions
+ * @param prefGroups The list of preference group descriptions
+ */
+ public synchronized void updateDescriptors(
+ Map<String, DeclareStyleableInfo> searchableStyleMap,
+ Map<String, DeclareStyleableInfo> appWidgetStyleMap,
+ ViewClassInfo[] prefs, ViewClassInfo[] prefGroups) {
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(ANDROID_NS_NAME,
+ ANDROID_URI);
+
+ ElementDescriptor searchable = createSearchable(searchableStyleMap, xmlns);
+ ElementDescriptor appWidget = createAppWidgetProviderInfo(appWidgetStyleMap, xmlns);
+ ElementDescriptor preferences = createPreference(prefs, prefGroups, xmlns);
+ ArrayList<ElementDescriptor> list = new ArrayList<ElementDescriptor>();
+ if (searchable != null) {
+ list.add(searchable);
+ mSearchDescriptor.setChildren(new ElementDescriptor[]{ searchable });
+ }
+ if (appWidget != null) {
+ list.add(appWidget);
+ mAppWidgetDescriptor.setChildren(new ElementDescriptor[]{ appWidget });
+ }
+ if (preferences != null) {
+ list.add(preferences);
+ mPrefDescriptor.setChildren(new ElementDescriptor[]{ preferences });
+ }
+
+ if (list.size() > 0) {
+ mDescriptor.setChildren(list.toArray(new ElementDescriptor[list.size()]));
+ }
+ }
+
+ //-------------------------
+ // Creation of <searchable>
+ //-------------------------
+
+ /**
+ * Returns the new ElementDescriptor for <searchable>
+ */
+ private ElementDescriptor createSearchable(
+ Map<String, DeclareStyleableInfo> searchableStyleMap,
+ XmlnsAttributeDescriptor xmlns) {
+
+ ElementDescriptor action_key = createElement(searchableStyleMap,
+ "SearchableActionKey", //$NON-NLS-1$ styleName
+ "actionkey", //$NON-NLS-1$ xmlName
+ "Action Key", // uiName
+ null, // sdk url
+ null, // extraAttribute
+ null, // childrenElements
+ false /* mandatory */ );
+
+ ElementDescriptor searchable = createElement(searchableStyleMap,
+ "Searchable", //$NON-NLS-1$ styleName
+ "searchable", //$NON-NLS-1$ xmlName
+ "Searchable", // uiName
+ null, // sdk url
+ xmlns, // extraAttribute
+ new ElementDescriptor[] { action_key }, // childrenElements
+ false /* mandatory */ );
+ return searchable;
+ }
+
+ /**
+ * Returns the new ElementDescriptor for <appwidget-provider>
+ */
+ private ElementDescriptor createAppWidgetProviderInfo(
+ Map<String, DeclareStyleableInfo> appWidgetStyleMap,
+ XmlnsAttributeDescriptor xmlns) {
+
+ if (appWidgetStyleMap == null) {
+ return null;
+ }
+
+ ElementDescriptor appWidget = createElement(appWidgetStyleMap,
+ "AppWidgetProviderInfo", //$NON-NLS-1$ styleName
+ "appwidget-provider", //$NON-NLS-1$ xmlName
+ "AppWidget Provider", // uiName
+ null, // sdk url
+ xmlns, // extraAttribute
+ null, // childrenElements
+ false /* mandatory */ );
+ return appWidget;
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from the information given here
+ * and the javadoc & attributes extracted from the style map if any.
+ */
+ private ElementDescriptor createElement(
+ Map<String, DeclareStyleableInfo> styleMap, String styleName,
+ String xmlName, String uiName, String sdkUrl,
+ AttributeDescriptor extraAttribute,
+ ElementDescriptor[] childrenElements, boolean mandatory) {
+
+ ElementDescriptor element = new ElementDescriptor(xmlName, uiName, null, sdkUrl,
+ null, childrenElements, mandatory);
+
+ return updateElement(element, styleMap, styleName, extraAttribute);
+ }
+
+ /**
+ * Updates an ElementDescriptor with the javadoc & attributes extracted from the style
+ * map if any.
+ */
+ private ElementDescriptor updateElement(ElementDescriptor element,
+ Map<String, DeclareStyleableInfo> styleMap,
+ String styleName,
+ AttributeDescriptor extraAttribute) {
+ ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>();
+
+ DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null;
+ if (style != null) {
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ element.setTooltip(style.getJavaDoc());
+ }
+
+ if (extraAttribute != null) {
+ descs.add(extraAttribute);
+ }
+
+ element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()]));
+ return element;
+ }
+
+ //--------------------------
+ // Creation of <Preferences>
+ //--------------------------
+
+ /**
+ * Returns the new ElementDescriptor for <Preferences>
+ */
+ private ElementDescriptor createPreference(ViewClassInfo[] prefs,
+ ViewClassInfo[] prefGroups, XmlnsAttributeDescriptor xmlns) {
+
+ ArrayList<ElementDescriptor> newPrefs = new ArrayList<ElementDescriptor>();
+ if (prefs != null) {
+ for (ViewClassInfo info : prefs) {
+ ElementDescriptor desc = convertPref(info);
+ newPrefs.add(desc);
+ }
+ }
+
+ ElementDescriptor topPreferences = null;
+
+ ArrayList<ElementDescriptor> newGroups = new ArrayList<ElementDescriptor>();
+ if (prefGroups != null) {
+ for (ViewClassInfo info : prefGroups) {
+ ElementDescriptor desc = convertPref(info);
+ newGroups.add(desc);
+
+ if (info.getFullClassName() == SdkConstants.CLASS_PREFERENCES) {
+ topPreferences = desc;
+ }
+ }
+ }
+
+ ArrayList<ElementDescriptor> everything = new ArrayList<ElementDescriptor>();
+ everything.addAll(newGroups);
+ everything.addAll(newPrefs);
+ ElementDescriptor[] newArray = everything.toArray(new ElementDescriptor[everything.size()]);
+
+ // Link all groups to everything else here.. recursively
+ for (ElementDescriptor layoutDesc : newGroups) {
+ layoutDesc.setChildren(newArray);
+ }
+
+ // The "top" element to be returned corresponds to the class "Preferences".
+ // Its descriptor has already been created. However the root one also needs
+ // the hidden xmlns:android definition..
+ if (topPreferences != null) {
+ AttributeDescriptor[] attrs = topPreferences.getAttributes();
+ AttributeDescriptor[] newAttrs = new AttributeDescriptor[attrs.length + 1];
+ System.arraycopy(attrs, 0, newAttrs, 0, attrs.length);
+ newAttrs[attrs.length] = xmlns;
+ return new ElementDescriptor(
+ topPreferences.getXmlName(),
+ topPreferences.getUiName(),
+ topPreferences.getTooltip(),
+ topPreferences.getSdkUrl(),
+ newAttrs,
+ topPreferences.getChildren(),
+ false /* mandatory */);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Creates an element descriptor from a given {@link ViewClassInfo}.
+ */
+ private ElementDescriptor convertPref(ViewClassInfo info) {
+ String xml_name = info.getShortClassName();
+ String tooltip = info.getJavaDoc();
+
+ // Process all Preference attributes
+ ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ info.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+
+ for (ViewClassInfo link = info.getSuperClass();
+ link != null;
+ link = link.getSuperClass()) {
+ AttributeInfo[] attrList = link.getAttributes();
+ if (attrList.length > 0) {
+ attributes.add(new SeparatorAttributeDescriptor(
+ String.format("Attributes from %1$s", link.getShortClassName())));
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ attrList,
+ null, // requiredAttributes
+ null); // overrides
+ }
+ }
+
+ return new ViewElementDescriptor(xml_name,
+ xml_name, // ui_name
+ info.getFullClassName(),
+ tooltip,
+ null, // sdk_url
+ attributes.toArray(new AttributeDescriptor[attributes.size()]),
+ null,
+ null, // children
+ false /* mandatory */);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java
new file mode 100644
index 000000000..baf8a1039
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.SdkConstants;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.jface.viewers.DialogCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+import java.text.MessageFormat;
+
+/**
+ * Custom DialogCellEditor, replacing the Label with an editable {@link Text} widget.
+ * <p/>Also set the button to {@link SWT#FLAT} to make sure it looks good on MacOS X.
+ * <p/>Most of the code comes from TextCellEditor.
+ */
+public abstract class EditableDialogCellEditor extends DialogCellEditor {
+
+ private Text text;
+
+ private ModifyListener modifyListener;
+
+ /**
+ * State information for updating action enablement
+ */
+ private boolean isSelection = false;
+
+ private boolean isDeleteable = false;
+
+ private boolean isSelectable = false;
+
+ EditableDialogCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ /*
+ * Re-implement this method only to properly set the style in the button, or it won't look
+ * good in MacOS X
+ */
+ @Override
+ protected Button createButton(Composite parent) {
+ Button result = new Button(parent, SWT.DOWN | SWT.FLAT);
+ result.setText("..."); //$NON-NLS-1$
+ return result;
+ }
+
+
+ @Override
+ protected Control createContents(Composite cell) {
+ text = new Text(cell, SWT.SINGLE);
+ text.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ handleDefaultSelection(e);
+ }
+ });
+ text.addKeyListener(new KeyAdapter() {
+ // hook key pressed - see PR 14201
+ @Override
+ public void keyPressed(KeyEvent e) {
+ keyReleaseOccured(e);
+
+ // as a result of processing the above call, clients may have
+ // disposed this cell editor
+ if ((getControl() == null) || getControl().isDisposed()) {
+ return;
+ }
+ checkSelection(); // see explanation below
+ checkDeleteable();
+ checkSelectable();
+ }
+ });
+ text.addTraverseListener(new TraverseListener() {
+ @Override
+ public void keyTraversed(TraverseEvent e) {
+ if (e.detail == SWT.TRAVERSE_ESCAPE
+ || e.detail == SWT.TRAVERSE_RETURN) {
+ e.doit = false;
+ }
+ }
+ });
+ // We really want a selection listener but it is not supported so we
+ // use a key listener and a mouse listener to know when selection changes
+ // may have occurred
+ text.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+ });
+ text.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ EditableDialogCellEditor.this.focusLost();
+ }
+ });
+ text.setFont(cell.getFont());
+ text.setBackground(cell.getBackground());
+ text.setText("");//$NON-NLS-1$
+ text.addModifyListener(getModifyListener());
+ return text;
+ }
+
+ /**
+ * Checks to see if the "deletable" state (can delete/
+ * nothing to delete) has changed and if so fire an
+ * enablement changed notification.
+ */
+ private void checkDeleteable() {
+ boolean oldIsDeleteable = isDeleteable;
+ isDeleteable = isDeleteEnabled();
+ if (oldIsDeleteable != isDeleteable) {
+ fireEnablementChanged(DELETE);
+ }
+ }
+
+ /**
+ * Checks to see if the "selectable" state (can select)
+ * has changed and if so fire an enablement changed notification.
+ */
+ private void checkSelectable() {
+ boolean oldIsSelectable = isSelectable;
+ isSelectable = isSelectAllEnabled();
+ if (oldIsSelectable != isSelectable) {
+ fireEnablementChanged(SELECT_ALL);
+ }
+ }
+
+ /**
+ * Checks to see if the selection state (selection /
+ * no selection) has changed and if so fire an
+ * enablement changed notification.
+ */
+ private void checkSelection() {
+ boolean oldIsSelection = isSelection;
+ isSelection = text.getSelectionCount() > 0;
+ if (oldIsSelection != isSelection) {
+ fireEnablementChanged(COPY);
+ fireEnablementChanged(CUT);
+ }
+ }
+
+ /* (non-Javadoc)
+ * Method declared on CellEditor.
+ */
+ @Override
+ protected void doSetFocus() {
+ if (text != null) {
+ text.selectAll();
+ text.setFocus();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.jface.viewers.DialogCellEditor#updateContents(java.lang.Object)
+ */
+ @Override
+ protected void updateContents(Object value) {
+ Assert.isTrue(text != null && (value == null || (value instanceof String)));
+ if (value != null) {
+ text.removeModifyListener(getModifyListener());
+ text.setText((String) value);
+ text.addModifyListener(getModifyListener());
+ }
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of
+ * this <code>CellEditor</code> framework method returns
+ * the text string.
+ *
+ * @return the text string
+ */
+ @Override
+ protected Object doGetValue() {
+ return text.getText();
+ }
+
+
+ /**
+ * Processes a modify event that occurred in this text cell editor.
+ * This framework method performs validation and sets the error message
+ * accordingly, and then reports a change via <code>fireEditorValueChanged</code>.
+ * Subclasses should call this method at appropriate times. Subclasses
+ * may extend or reimplement.
+ *
+ * @param e the SWT modify event
+ */
+ protected void editOccured(ModifyEvent e) {
+ String value = text.getText();
+ if (value == null) {
+ value = "";//$NON-NLS-1$
+ }
+ Object typedValue = value;
+ boolean oldValidState = isValueValid();
+ boolean newValidState = isCorrect(typedValue);
+
+ if (!newValidState) {
+ // try to insert the current value into the error message.
+ setErrorMessage(MessageFormat.format(getErrorMessage(),
+ new Object[] { value }));
+ }
+ valueChanged(oldValidState, newValidState);
+ }
+
+ /**
+ * Return the modify listener.
+ */
+ private ModifyListener getModifyListener() {
+ if (modifyListener == null) {
+ modifyListener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ editOccured(e);
+ }
+ };
+ }
+ return modifyListener;
+ }
+
+ /**
+ * Handles a default selection event from the text control by applying the editor
+ * value and deactivating this cell editor.
+ *
+ * @param event the selection event
+ *
+ * @since 3.0
+ */
+ protected void handleDefaultSelection(SelectionEvent event) {
+ // same with enter-key handling code in keyReleaseOccured(e);
+ fireApplyEditorValue();
+ deactivate();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code> if
+ * the current selection is not empty.
+ */
+ @Override
+ public boolean isCopyEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0;
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code> if
+ * the current selection is not empty.
+ */
+ @Override
+ public boolean isCutEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0;
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code>
+ * if there is a selection or if the caret is not positioned
+ * at the end of the text.
+ */
+ @Override
+ public boolean isDeleteEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0
+ || text.getCaretPosition() < text.getCharCount();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method always returns <code>true</code>.
+ */
+ @Override
+ public boolean isPasteEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if save all is enabled
+ * @return true if it is
+ */
+ public boolean isSaveAllEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if this cell editor is
+ * able to perform the select all action.
+ * <p>
+ * This default implementation always returns
+ * <code>false</code>.
+ * </p>
+ * <p>
+ * Subclasses may override
+ * </p>
+ * @return <code>true</code> if select all is possible,
+ * <code>false</code> otherwise
+ */
+ @Override
+ public boolean isSelectAllEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getCharCount() > 0;
+ }
+
+ /**
+ * Processes a key release event that occurred in this cell editor.
+ * <p>
+ * The <code>TextCellEditor</code> implementation of this framework method
+ * ignores when the RETURN key is pressed since this is handled in
+ * <code>handleDefaultSelection</code>.
+ * An exception is made for Ctrl+Enter for multi-line texts, since
+ * a default selection event is not sent in this case.
+ * </p>
+ *
+ * @param keyEvent the key event
+ */
+ @Override
+ protected void keyReleaseOccured(KeyEvent keyEvent) {
+ if (keyEvent.character == '\r') { // Return key
+ // Enter is handled in handleDefaultSelection.
+ // Do not apply the editor value in response to an Enter key event
+ // since this can be received from the IME when the intent is -not-
+ // to apply the value.
+ // See bug 39074 [CellEditors] [DBCS] canna input mode fires bogus event from Text Control
+ //
+ // An exception is made for Ctrl+Enter for multi-line texts, since
+ // a default selection event is not sent in this case.
+ if (text != null && !text.isDisposed()
+ && (text.getStyle() & SWT.MULTI) != 0) {
+ if ((keyEvent.stateMask & SWT.CTRL) != 0) {
+ super.keyReleaseOccured(keyEvent);
+ }
+ }
+ return;
+ }
+ super.keyReleaseOccured(keyEvent);
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method copies the
+ * current selection to the clipboard.
+ */
+ @Override
+ public void performCopy() {
+ text.copy();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method cuts the
+ * current selection to the clipboard.
+ */
+ @Override
+ public void performCut() {
+ text.cut();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method deletes the
+ * current selection or, if there is no selection,
+ * the character next character from the current position.
+ */
+ @Override
+ public void performDelete() {
+ if (text.getSelectionCount() > 0) {
+ // remove the contents of the current selection
+ text.insert(""); //$NON-NLS-1$
+ } else {
+ // remove the next character
+ int pos = text.getCaretPosition();
+ if (pos < text.getCharCount()) {
+ text.setSelection(pos, pos + 1);
+ text.insert(""); //$NON-NLS-1$
+ }
+ }
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method pastes the
+ * the clipboard contents over the current selection.
+ */
+ @Override
+ public void performPaste() {
+ text.paste();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method selects all of the
+ * current text.
+ */
+ @Override
+ public void performSelectAll() {
+ text.selectAll();
+ checkSelection();
+ checkDeleteable();
+ }
+
+ @Override
+ protected void focusLost() {
+ if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_LINUX) {
+ // On Linux, something about the order of focus event delivery prevents the
+ // callback on the "..." button to be invoked, which means the
+ // customizer dialog never shows up (see issue #18348).
+ // (Note that simply trying to Display.asyncRun() the super.focusLost()
+ // method does not work.)
+ //
+ // We can work around this by not deactivating on a focus loss.
+ // This means that in some cases the cell editor will still be
+ // shown in the property sheet, but I've tested that the values
+ // are all committed as before. This is better than having a non-operational
+ // customizer, but since this issue only happens on Linux the workaround
+ // is only done on Linux such that on other platforms we deactivate
+ // immediately on focus loss.
+ //
+ if (isActivated()) {
+ fireApplyEditorValue();
+ // super.focusLost calls the following which we're deliberately
+ // suppressing here:
+ // deactivate();
+ }
+ } else {
+ super.focusLost();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java
new file mode 100644
index 000000000..7085e5d50
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java
@@ -0,0 +1,72 @@
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_ERROR;
+import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_WARNING;
+import static org.eclipse.ui.ISharedImages.IMG_OBJS_ERROR_TSK;
+import static org.eclipse.ui.ISharedImages.IMG_OBJS_WARN_TSK;
+
+import org.eclipse.jface.resource.CompositeImageDescriptor;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.DecorationOverlayIcon;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * ImageDescriptor that adds a error marker.
+ * Based on {@link DecorationOverlayIcon} only available in Eclipse 3.3
+ */
+public class ErrorImageComposite extends CompositeImageDescriptor {
+
+ private Image mBaseImage;
+ private ImageDescriptor mErrorImageDescriptor;
+ private Point mSize;
+
+ /**
+ * Creates a new {@link ErrorImageComposite}
+ *
+ * @param baseImage the base image to overlay an icon on top of
+ */
+ public ErrorImageComposite(Image baseImage) {
+ this(baseImage, false);
+ }
+
+ /**
+ * Creates a new {@link ErrorImageComposite}
+ *
+ * @param baseImage the base image to overlay an icon on top of
+ * @param warning if true, add a warning icon, otherwise an error icon
+ */
+ public ErrorImageComposite(Image baseImage, boolean warning) {
+ mBaseImage = baseImage;
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ mErrorImageDescriptor = sharedImages.getImageDescriptor(
+ warning ? IMG_DEC_FIELD_WARNING : IMG_DEC_FIELD_ERROR);
+ if (mErrorImageDescriptor == null) {
+ mErrorImageDescriptor = sharedImages.getImageDescriptor(
+ warning ? IMG_OBJS_WARN_TSK : IMG_OBJS_ERROR_TSK);
+ }
+ mSize = new Point(baseImage.getBounds().width, baseImage.getBounds().height);
+ }
+
+ @Override
+ protected void drawCompositeImage(int width, int height) {
+ ImageData baseData = mBaseImage.getImageData();
+ drawImage(baseData, 0, 0);
+
+ ImageData overlayData = mErrorImageDescriptor.getImageData();
+ if (overlayData.width == baseData.width && overlayData.height == baseData.height) {
+ overlayData = overlayData.scaledTo(14, 14);
+ drawImage(overlayData, -3, mSize.y - overlayData.height + 3);
+ } else {
+ drawImage(overlayData, 0, mSize.y - overlayData.height);
+ }
+ }
+
+ @Override
+ protected Point getSize() {
+ return mSize;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java
new file mode 100644
index 000000000..2a1bc36b5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ * <p/>The dialog box opened is the same as the one in the ui created by
+ * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)}
+ */
+public class FlagValueCellEditor extends EditableDialogCellEditor {
+
+ private UiFlagAttributeNode mUiFlagAttribute;
+
+ public FlagValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Object openDialogBox(Control cellEditorWindow) {
+ if (mUiFlagAttribute != null) {
+ String currentValue = (String)getValue();
+ return mUiFlagAttribute.showDialog(cellEditorWindow.getShell(), currentValue);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiFlagAttributeNode) {
+ mUiFlagAttribute = (UiFlagAttributeNode)value;
+ super.doSetValue(mUiFlagAttribute.getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java
new file mode 100644
index 000000000..0c780a8c7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.ComboBoxCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * ComboBoxCellEditor able to receive a {@link UiListAttributeNode} in the {@link #setValue(Object)}
+ * method, and returning a {@link String} in {@link #getValue()} instead of an {@link Integer}.
+ */
+public class ListValueCellEditor extends ComboBoxCellEditor {
+ private String[] mItems;
+ private CCombo mCombo;
+
+ public ListValueCellEditor(Composite parent) {
+ super(parent, new String[0], SWT.DROP_DOWN);
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ mCombo = (CCombo) super.createControl(parent);
+ return mCombo;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiListAttributeNode) {
+ UiListAttributeNode uiListAttribute = (UiListAttributeNode)value;
+
+ // set the possible values in the combo
+ String[] items = uiListAttribute.getPossibleValues(null);
+ mItems = new String[items.length];
+ System.arraycopy(items, 0, mItems, 0, items.length);
+ setItems(mItems);
+
+ // now edit the current value of the attribute
+ String attrValue = uiListAttribute.getCurrentValue();
+ mCombo.setText(attrValue);
+
+ return;
+ }
+
+ // default behavior
+ super.doSetValue(value);
+ }
+
+ @Override
+ protected Object doGetValue() {
+ String comboText = mCombo.getText();
+ if (comboText == null) {
+ return ""; //$NON-NLS-1$
+ }
+ return comboText;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java
new file mode 100644
index 000000000..8efe294b1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ * <p/>The dialog box opened is the same as the one in the ui created by
+ * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)}
+ */
+public class ResourceValueCellEditor extends EditableDialogCellEditor {
+
+ private UiResourceAttributeNode mUiResourceAttribute;
+
+ public ResourceValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Object openDialogBox(Control cellEditorWindow) {
+ if (mUiResourceAttribute != null) {
+ String currentValue = (String)getValue();
+ return mUiResourceAttribute.showDialog(cellEditorWindow.getShell(), currentValue);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiResourceAttributeNode) {
+ mUiResourceAttribute = (UiResourceAttributeNode)value;
+ super.doSetValue(mUiResourceAttribute.getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java
new file mode 100644
index 000000000..fdb5d8292
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+
+import org.eclipse.jface.text.DefaultInformationControl;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.SectionPart;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+
+import java.lang.reflect.Method;
+
+/**
+ * Helper for the AndroidManifest form editor.
+ *
+ * Helps create a new SectionPart with sensible default parameters,
+ * create default layout or add typical widgets.
+ *
+ * IMPORTANT: This is NOT a generic class. It makes a lot of assumptions on the
+ * UI as used by the form editor for the AndroidManifest.
+ *
+ * TODO: Consider moving to a common package.
+ */
+public final class SectionHelper {
+
+ /**
+ * Utility class that derives from SectionPart, constructs the Section with
+ * sensible defaults (with a title and a description) and provide some shorthand
+ * methods for creating typically UI (label and text, form text.)
+ */
+ static public class ManifestSectionPart extends SectionPart {
+
+ /**
+ * Construct a SectionPart that uses a title bar and a description.
+ * It's up to the caller to call setText() and setDescription().
+ * <p/>
+ * The section style includes a description and a title bar by default.
+ *
+ * @param body The parent (e.g. FormPage body)
+ * @param toolkit Form Toolkit
+ */
+ public ManifestSectionPart(Composite body, FormToolkit toolkit) {
+ this(body, toolkit, 0, false);
+ }
+
+ /**
+ * Construct a SectionPart that uses a title bar and a description.
+ * It's up to the caller to call setText() and setDescription().
+ * <p/>
+ * The section style includes a description and a title bar by default.
+ * You can add extra styles, like Section.TWISTIE.
+ *
+ * @param body The parent (e.g. FormPage body).
+ * @param toolkit Form Toolkit.
+ * @param extra_style Extra styles (on top of description and title bar).
+ * @param use_description True if the Section.DESCRIPTION style should be added.
+ */
+ public ManifestSectionPart(Composite body, FormToolkit toolkit,
+ int extra_style, boolean use_description) {
+ super(body, toolkit, extra_style |
+ Section.TITLE_BAR |
+ (use_description ? Section.DESCRIPTION : 0));
+ }
+
+ // Create non-static methods of helpers just for convenience
+
+ /**
+ * Creates a new composite with a TableWrapLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ public Composite createTableLayout(FormToolkit toolkit, int numColumns) {
+ return SectionHelper.createTableLayout(getSection(), toolkit, numColumns);
+ }
+
+ /**
+ * Creates a label widget.
+ * If the parent layout if a TableWrapLayout, maximize it to span over all columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label The string for the label.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created label
+ */
+ public Label createLabel(Composite parent, FormToolkit toolkit, String label,
+ String tooltip) {
+ return SectionHelper.createLabel(parent, toolkit, label, tooltip);
+ }
+
+ /**
+ * Creates two widgets: a label and a text field.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label The string for the label.
+ * @param value The initial value of the text field. Can be null.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created Text field (the label is not returned)
+ */
+ public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label,
+ String value, String tooltip) {
+ return SectionHelper.createLabelAndText(parent, toolkit, label, value, tooltip);
+ }
+
+ /**
+ * Creates a FormText widget.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param isHtml True if the form text will contain HTML that must be interpreted as
+ * rich text (i.e. parse tags & expand URLs).
+ * @param label The string for the label.
+ * @param setupLayoutData indicates whether the created form text receives a TableWrapData
+ * through the setLayoutData method. In some case, creating it will make the table parent
+ * huge, which we don't want.
+ * @return The new created FormText.
+ */
+ public FormText createFormText(Composite parent, FormToolkit toolkit, boolean isHtml,
+ String label, boolean setupLayoutData) {
+ return SectionHelper.createFormText(parent, toolkit, isHtml, label, setupLayoutData);
+ }
+
+ /**
+ * Forces the section to recompute its layout and redraw.
+ * This is needed after the content of the section has been changed at runtime.
+ */
+ public void layoutChanged() {
+ Section section = getSection();
+
+ // Calls getSection().reflow(), which is the same that Section calls
+ // when the expandable state is changed and the height changes.
+ // Since this is protected, some reflection is needed to invoke it.
+ try {
+ Method reflow;
+ reflow = section.getClass().getDeclaredMethod("reflow", (Class<?>[])null);
+ reflow.setAccessible(true);
+ reflow.invoke(section);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Error when invoking Section.reflow");
+ }
+
+ section.layout(true /* changed */, true /* all */);
+ }
+ }
+
+ /**
+ * Creates a new composite with a TableWrapLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param composite The parent (e.g. a Section or SectionPart)
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ static public Composite createTableLayout(Composite composite, FormToolkit toolkit,
+ int numColumns) {
+ Composite table = toolkit.createComposite(composite);
+ TableWrapLayout layout = new TableWrapLayout();
+ layout.numColumns = numColumns;
+ table.setLayout(layout);
+ toolkit.paintBordersFor(table);
+ if (composite instanceof Section) {
+ ((Section) composite).setClient(table);
+ }
+ return table;
+ }
+
+ /**
+ * Creates a new composite with a GridLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param composite The parent (e.g. a Section or SectionPart)
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ static public Composite createGridLayout(Composite composite, FormToolkit toolkit,
+ int numColumns) {
+ Composite grid = toolkit.createComposite(composite);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = numColumns;
+ grid.setLayout(layout);
+ toolkit.paintBordersFor(grid);
+ if (composite instanceof Section) {
+ ((Section) composite).setClient(grid);
+ }
+ return grid;
+ }
+
+ /**
+ * Creates two widgets: a label and a text field.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label_text The string for the label.
+ * @param value The initial value of the text field. Can be null.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created Text field (the label is not returned)
+ */
+ static public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label_text,
+ String value, String tooltip) {
+ Label label = toolkit.createLabel(parent, label_text);
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ Text text = toolkit.createText(parent, value);
+ text.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+
+ addControlTooltip(label, tooltip);
+ return text;
+ }
+
+ /**
+ * Creates a label widget.
+ * If the parent layout if a TableWrapLayout, maximize it to span over all columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label_text The string for the label.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created label
+ */
+ static public Label createLabel(Composite parent, FormToolkit toolkit, String label_text,
+ String tooltip) {
+ Label label = toolkit.createLabel(parent, label_text);
+
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ label.setLayoutData(twd);
+
+ addControlTooltip(label, tooltip);
+ return label;
+ }
+
+ /**
+ * Associates a tooltip with a control.
+ *
+ * This mirrors the behavior from org.eclipse.pde.internal.ui.editor.text.PDETextHover
+ *
+ * @param control The control to which associate the tooltip.
+ * @param tooltip The tooltip string. Can use \n for multi-lines. Will not display if null.
+ */
+ static public void addControlTooltip(final Control control, String tooltip) {
+ if (control == null || tooltip == null || tooltip.length() == 0) {
+ return;
+ }
+
+ // Some kinds of controls already properly implement tooltip display.
+ if (control instanceof Button) {
+ control.setToolTipText(tooltip);
+ return;
+ }
+
+ control.setToolTipText(null);
+
+ final DefaultInformationControl ic = new DefaultInformationControl(control.getShell());
+ ic.setInformation(tooltip);
+ Point sz = ic.computeSizeHint();
+ ic.setSize(sz.x, sz.y);
+ ic.setVisible(false); // initially hidden
+
+ control.addMouseTrackListener(new MouseTrackListener() {
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ ic.setVisible(false);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ ic.setLocation(control.toDisplay(10, 25)); // same offset as in PDETextHover
+ ic.setVisible(true);
+ }
+ });
+ control.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ ic.dispose();
+ }
+ });
+ }
+
+ /**
+ * Creates a FormText widget.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param isHtml True if the form text will contain HTML that must be interpreted as
+ * rich text (i.e. parse tags & expand URLs).
+ * @param label The string for the label.
+ * @param setupLayoutData indicates whether the created form text receives a TableWrapData
+ * through the setLayoutData method. In some case, creating it will make the table parent
+ * huge, which we don't want.
+ * @return The new created FormText.
+ */
+ static public FormText createFormText(Composite parent, FormToolkit toolkit,
+ boolean isHtml, String label, boolean setupLayoutData) {
+ FormText text = toolkit.createFormText(parent, true /* track focus */);
+ if (setupLayoutData) {
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ text.setLayoutData(twd);
+ }
+ text.setWhitespaceNormalized(true);
+ if (isHtml && !label.startsWith("<form>")) { //$NON-NLS-1$
+ // This assertion is violated, for example by the Class attribute for an activity
+ //assert label.startsWith("<form>") : "HTML for FormText must be wrapped in <form>...</form>"; //$NON-NLS-1$
+ label = "<form>" + label + "</form>"; //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ text.setText(label, isHtml /* parseTags */, isHtml /* expandURLs */);
+ return text;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java
new file mode 100644
index 000000000..3750c3472
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * TextCellEditor able to receive a {@link UiAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ */
+public class TextValueCellEditor extends TextCellEditor {
+
+ public TextValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiAttributeNode) {
+ super.doSetValue(((UiAttributeNode)value).getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java
new file mode 100644
index 000000000..db9fa069f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Generic page's section part that displays all attributes of a given {@link UiElementNode}.
+ * <p/>
+ * This part is designed to be displayed in a page that has a table column layout.
+ * It is linked to a specific {@link UiElementNode} and automatically displays all of its
+ * attributes, manages its dirty state and commits the attributes when necessary.
+ * <p/>
+ * No derivation is needed unless the UI or workflow needs to be extended.
+ */
+public class UiElementPart extends ManifestSectionPart {
+
+ /** A reference to the container editor */
+ private ManifestEditor mEditor;
+ /** The {@link UiElementNode} manipulated by this SectionPart. It can be null. */
+ private UiElementNode mUiElementNode;
+ /** Table that contains all the attributes */
+ private Composite mTable;
+
+ public UiElementPart(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode uiElementNode, String sectionTitle, String sectionDescription,
+ int extra_style) {
+ super(body, toolkit, extra_style, sectionDescription != null);
+ mEditor = editor;
+ mUiElementNode = uiElementNode;
+ setupSection(sectionTitle, sectionDescription);
+
+ if (uiElementNode == null) {
+ // This is serious and should never happen. Instead of crashing, simply abort.
+ // There will be no UI, which will prevent further damage.
+ AdtPlugin.log(IStatus.ERROR, "Missing node to edit!"); //$NON-NLS-1$
+ return;
+ }
+ }
+
+ /**
+ * Returns the Editor associated with this part.
+ */
+ public ManifestEditor getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Returns the {@link UiElementNode} associated with this part.
+ */
+ public UiElementNode getUiElementNode() {
+ return mUiElementNode;
+ }
+
+ /**
+ * Changes the element node handled by this part.
+ *
+ * @param uiElementNode The new element node for the part.
+ */
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ mUiElementNode = uiElementNode;
+ }
+
+ /**
+ * Initializes the form part.
+ * <p/>
+ * This is called by the owning managed form as soon as the part is added to the form,
+ * which happens right after the part is actually created.
+ */
+ @Override
+ public void initialize(IManagedForm form) {
+ super.initialize(form);
+ createFormControls(form);
+ }
+
+ /**
+ * Setup the section that contains this part.
+ * <p/>
+ * This is called by the constructor to set the section's title and description
+ * with parameters given in the constructor.
+ * <br/>
+ * Derived class override this if needed, however in most cases the default
+ * implementation should be enough.
+ *
+ * @param sectionTitle The section part's title
+ * @param sectionDescription The section part's description
+ */
+ protected void setupSection(String sectionTitle, String sectionDescription) {
+ Section section = getSection();
+ section.setText(sectionTitle);
+ section.setDescription(sectionDescription);
+ }
+
+ /**
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ * <p/>
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The owner managed form
+ */
+ protected void createFormControls(IManagedForm managedForm) {
+ setTable(createTableLayout(managedForm.getToolkit(), 2 /* numColumns */));
+
+ createUiAttributes(managedForm);
+ }
+
+ /**
+ * Sets the table where the attribute UI needs to be created.
+ */
+ protected void setTable(Composite table) {
+ mTable = table;
+ }
+
+ /**
+ * Returns the table where the attribute UI needs to be created.
+ */
+ protected Composite getTable() {
+ return mTable;
+ }
+
+ /**
+ * Add all the attribute UI widgets into the underlying table layout.
+ *
+ * @param managedForm The owner managed form
+ */
+ protected void createUiAttributes(IManagedForm managedForm) {
+ Composite table = getTable();
+ if (table == null || managedForm == null) {
+ return;
+ }
+
+ // Remove any old UI controls
+ for (Control c : table.getChildren()) {
+ c.dispose();
+ }
+
+ fillTable(table, managedForm);
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * Actually fills the table.
+ * This is called by {@link #createUiAttributes(IManagedForm)} to populate the new
+ * table. The default implementation is to use
+ * {@link #insertUiAttributes(UiElementNode, Composite, IManagedForm)} to actually
+ * place the attributes of the default {@link UiElementNode} in the table.
+ * <p/>
+ * Derived classes can override this to add controls in the table before or after.
+ *
+ * @param table The table to fill. It must have 2 columns.
+ * @param managedForm The managed form for new controls.
+ */
+ protected void fillTable(Composite table, IManagedForm managedForm) {
+ int inserted = insertUiAttributes(mUiElementNode, table, managedForm);
+
+ if (inserted == 0) {
+ createLabel(table, managedForm.getToolkit(),
+ "No attributes to display, waiting for SDK to finish loading...",
+ null /* tooltip */ );
+ }
+ }
+
+ /**
+ * Insert the UI attributes of the given {@link UiElementNode} in the given table.
+ *
+ * @param uiNode The {@link UiElementNode} that contains the attributes to display.
+ * Must not be null.
+ * @param table The table to fill. It must have 2 columns.
+ * @param managedForm The managed form for new controls.
+ * @return The number of UI attributes inserted. It is >= 0.
+ */
+ protected int insertUiAttributes(UiElementNode uiNode, Composite table, IManagedForm managedForm) {
+ if (uiNode == null || table == null || managedForm == null) {
+ return 0;
+ }
+
+ // To iterate over all attributes, we use the {@link ElementDescriptor} instead
+ // of the {@link UiElementNode} because the attributes' order is guaranteed in the
+ // descriptor but not in the node itself.
+ AttributeDescriptor[] attr_desc_list = uiNode.getAttributeDescriptors();
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ }
+
+ UiAttributeNode ui_attr = uiNode.findUiAttribute(attr_desc);
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+ } else {
+ // The XML has an extra attribute which wasn't declared in
+ // AndroidManifestDescriptors. This is not a problem, we just ignore it.
+ AdtPlugin.log(IStatus.WARNING,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ uiNode.getDescriptor().getXmlName());
+ }
+ }
+ return attr_desc_list.length;
+ }
+
+ /**
+ * Tests whether the part is dirty i.e. its widgets have state that is
+ * newer than the data in the model.
+ * <p/>
+ * This is done by iterating over all attributes and updating the super's
+ * internal dirty flag. Stop once at least one attribute is dirty.
+ *
+ * @return <code>true</code> if the part is dirty, <code>false</code>
+ * otherwise.
+ */
+ @Override
+ public boolean isDirty() {
+ if (mUiElementNode != null && !super.isDirty()) {
+ for (UiAttributeNode ui_attr : mUiElementNode.getAllUiAttributes()) {
+ if (ui_attr.isDirty()) {
+ markDirty();
+ break;
+ }
+ }
+ }
+ return super.isDirty();
+ }
+
+ /**
+ * If part is displaying information loaded from a model, this method
+ * instructs it to commit the new (modified) data back into the model.
+ *
+ * @param onSave
+ * indicates if commit is called during 'save' operation or for
+ * some other reason (for example, if form is contained in a
+ * wizard or a multi-page editor and the user is about to leave
+ * the page).
+ */
+ @Override
+ public void commit(boolean onSave) {
+ if (mUiElementNode != null) {
+ mEditor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ for (UiAttributeNode ui_attr : mUiElementNode.getAllUiAttributes()) {
+ ui_attr.commit();
+ }
+ }
+ });
+ }
+
+ // We need to call super's commit after we synchronized the nodes to make sure we
+ // reset the dirty flag after all the side effects from committing have occurred.
+ super.commit(onSave);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java
new file mode 100644
index 000000000..3fe98bb23
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.apache.xml.serialize.Method;
+import org.apache.xml.serialize.OutputFormat;
+import org.apache.xml.serialize.XMLSerializer;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides Cut and Copy actions for the tree nodes.
+ */
+@SuppressWarnings({"restriction", "deprecation"})
+public class CopyCutAction extends Action {
+ private List<UiElementNode> mUiNodes;
+ private boolean mPerformCut;
+ private final AndroidXmlEditor mEditor;
+ private final Clipboard mClipboard;
+ private final ICommitXml mXmlCommit;
+
+ /**
+ * Creates a new Copy or Cut action.
+ *
+ * @param selected The UI node to cut or copy. It *must* have a non-null XML node.
+ * @param performCut True if the operation is cut, false if it is copy.
+ */
+ public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
+ UiElementNode selected, boolean performCut) {
+ this(editor, clipboard, xmlCommit, toList(selected), performCut);
+ }
+
+ /**
+ * Creates a new Copy or Cut action.
+ *
+ * @param selected The UI nodes to cut or copy. They *must* have a non-null XML node.
+ * The list becomes owned by the {@link CopyCutAction}.
+ * @param performCut True if the operation is cut, false if it is copy.
+ */
+ public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
+ List<UiElementNode> selected, boolean performCut) {
+ super(performCut ? "Cut" : "Copy");
+ mEditor = editor;
+ mClipboard = clipboard;
+ mXmlCommit = xmlCommit;
+
+ ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
+ if (performCut) {
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_HOVER));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_DISABLED));
+ } else {
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_HOVER));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_DISABLED));
+ }
+
+ mUiNodes = selected;
+ mPerformCut = performCut;
+ }
+
+ /**
+ * Performs the cut or copy action.
+ * First an XML serializer is used to turn the existing XML node into a valid
+ * XML fragment, which is added as text to the clipboard.
+ */
+ @Override
+ public void run() {
+ super.run();
+ if (mUiNodes == null || mUiNodes.size() < 1) {
+ return;
+ }
+
+ // Commit the current pages first, to make sure the XML is in sync.
+ // Committing may change the XML structure.
+ if (mXmlCommit != null) {
+ mXmlCommit.commitPendingXmlChanges();
+ }
+
+ StringBuilder allText = new StringBuilder();
+ ArrayList<UiElementNode> nodesToCut = mPerformCut ? new ArrayList<UiElementNode>() : null;
+
+ for (UiElementNode uiNode : mUiNodes) {
+ try {
+ Node xml_node = uiNode.getXmlNode();
+ if (xml_node == null) {
+ return;
+ }
+
+ String data = getXmlTextFromEditor(xml_node);
+
+ // In the unlikely event that IStructuredDocument failed to extract the text
+ // directly from the editor, try to fall back on a direct XML serialization
+ // of the XML node. This uses the generic Node interface with no SSE tricks.
+ if (data == null) {
+ data = getXmlTextFromSerialization(xml_node);
+ }
+
+ if (data != null) {
+ allText.append(data);
+ if (mPerformCut) {
+ // only remove notes to cut if we actually got some XML text from them
+ nodesToCut.add(uiNode);
+ }
+ }
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "CopyCutAction failed for UI node %1$s", //$NON-NLS-1$
+ uiNode.getBreadcrumbTrailDescription(true));
+ }
+ } // for uiNode
+
+ if (allText != null && allText.length() > 0) {
+ mClipboard.setContents(
+ new Object[] { allText.toString() },
+ new Transfer[] { TextTransfer.getInstance() });
+ if (mPerformCut) {
+ for (UiElementNode uiNode : nodesToCut) {
+ uiNode.deleteXmlNode();
+ }
+ }
+ }
+ }
+
+ /** Get the data directly from the editor. */
+ private String getXmlTextFromEditor(Node xml_node) {
+ String data = null;
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ IStructuredDocument sse_doc = mEditor.getStructuredDocument();
+ if (xml_node instanceof NodeContainer) {
+ // The easy way to get the source of an SSE XML node.
+ data = ((NodeContainer) xml_node).getSource();
+ } else if (xml_node instanceof IndexedRegion && sse_doc != null) {
+ // Try harder.
+ IndexedRegion region = (IndexedRegion) xml_node;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (end > start) {
+ data = sse_doc.get(start, end - start);
+ }
+ }
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ } finally {
+ model.releaseFromRead();
+ }
+ return data;
+ }
+
+ /**
+ * Direct XML serialization of the XML node.
+ * <p/>
+ * This uses the generic Node interface with no SSE tricks. It's however slower
+ * and doesn't respect formatting (since serialization is involved instead of reading
+ * the actual text buffer.)
+ */
+ private String getXmlTextFromSerialization(Node xml_node) throws IOException {
+ String data;
+ StringWriter sw = new StringWriter();
+ XMLSerializer serializer = new XMLSerializer(sw,
+ new OutputFormat(Method.XML,
+ OutputFormat.Defaults.Encoding /* utf-8 */,
+ true /* indent */));
+ // Serialize will throw an IOException if it fails.
+ serializer.serialize((Element) xml_node);
+ data = sw.toString();
+ return data;
+ }
+
+ /**
+ * Static helper class to wrap on node into a list for the constructors.
+ */
+ private static ArrayList<UiElementNode> toList(UiElementNode selected) {
+ ArrayList<UiElementNode> list = null;
+ if (selected != null) {
+ list = new ArrayList<UiElementNode>(1);
+ list.add(selected);
+ }
+ return list;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java
new file mode 100644
index 000000000..067d1459e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+/**
+ * Interface for an object that can commit its changes to the underlying XML model
+ */
+public interface ICommitXml {
+
+ /** Commits pending data to the underlying XML model. */
+ public void commitPendingXmlChanges();
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java
new file mode 100644
index 000000000..385a53a5f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
+import org.eclipse.ui.dialogs.ISelectionStatusValidator;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * A selection dialog to select the type of the new element node to
+ * create, either in the application node or the selected sub node.
+ */
+public class NewItemSelectionDialog extends AbstractElementListSelectionDialog {
+
+ /** The UI node selected in the tree view before creating the new item selection dialog.
+ * Can be null -- which means new items must be created in the root_node. */
+ private UiElementNode mSelectedUiNode;
+ /** The root node chosen by the user, either root_node or the one passed
+ * to the constructor if not null */
+ private UiElementNode mChosenRootNode;
+ private UiElementNode mLocalRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. Can be null. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The key for the {@link #setLastUsedXmlName(Object[])}. It corresponds to the full
+ * workspace path of the currently edited file, if this can be computed. This is computed
+ * by {@link #getLastUsedXmlName(UiElementNode)}, called from the constructor. */
+ private String mLastUsedKey;
+ /** A static map of known XML Names used for a given file. The map has full workspace
+ * paths as key and XML names as values. */
+ private static final Map<String, String> sLastUsedXmlName = new HashMap<String, String>();
+ /** The potential XML Name to initially select in the selection dialog. This is computed
+ * in the constructor and set by {@link #setInitialSelection(UiElementNode)}. */
+ private String mInitialXmlName;
+
+ /**
+ * Creates the new item selection dialog.
+ *
+ * @param shell The parent shell for the list.
+ * @param labelProvider ILabelProvider for the list.
+ * @param descriptorFilters The element allows at the root of the tree. Can be null.
+ * @param ui_node The selected node, or null if none is selected.
+ * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node.
+ */
+ public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider,
+ ElementDescriptor[] descriptorFilters,
+ UiElementNode ui_node,
+ UiElementNode root_node) {
+ super(shell, labelProvider);
+ mDescriptorFilters = descriptorFilters;
+ mLocalRootNode = root_node;
+
+ // Only accept the UI node if it is not the UI root node and it can have children.
+ // If the node cannot have children, select its parent as a potential target.
+ if (ui_node != null && ui_node != mLocalRootNode) {
+ if (ui_node.getDescriptor().hasChildren()) {
+ mSelectedUiNode = ui_node;
+ } else {
+ UiElementNode parent = ui_node.getUiParent();
+ if (parent != null && parent != mLocalRootNode) {
+ mSelectedUiNode = parent;
+ }
+ }
+ }
+
+ setHelpAvailable(false);
+ setMultipleSelection(false);
+
+ setValidator(new ISelectionStatusValidator() {
+ @Override
+ public IStatus validate(Object[] selection) {
+ if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) {
+ return new Status(IStatus.OK, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.OK, // code
+ ((ViewElementDescriptor) selection[0]).getFullClassName(), //msg
+ null); // exception
+ } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) {
+ return new Status(IStatus.OK, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.OK, // code
+ "", //$NON-NLS-1$ // msg
+ null); // exception
+ } else {
+ return new Status(IStatus.ERROR, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.ERROR, // code
+ "Invalid selection", // msg, translatable
+ null); // exception
+ }
+ }
+ });
+
+ // Determine the initial selection using a couple heuristics.
+
+ // First check if we can get the last used node type for this file.
+ // The heuristic is that generally one keeps adding the same kind of items to the
+ // same file, so reusing the last used item type makes most sense.
+ String xmlName = getLastUsedXmlName(root_node);
+ if (xmlName == null) {
+ // Another heuristic is to find the most used item and default to that.
+ xmlName = getMostUsedXmlName(root_node);
+ }
+ if (xmlName == null) {
+ // Finally the last heuristic is to see if there's an item with a name
+ // similar to the edited file name.
+ xmlName = getLeafFileName(root_node);
+ }
+ // Set the potential name. Selecting the right item is done later by setInitialSelection().
+ mInitialXmlName = xmlName;
+ }
+
+ /**
+ * Returns a potential XML name based on the file name.
+ * The item name is marked with an asterisk to identify it as a partial match.
+ */
+ private String getLeafFileName(UiElementNode ui_node) {
+ if (ui_node != null) {
+ AndroidXmlEditor editor = ui_node.getEditor();
+ if (editor != null) {
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ IFile f = ((FileEditorInput) editorInput).getFile();
+ if (f != null) {
+ String leafName = f.getFullPath().removeFileExtension().lastSegment();
+ return "*" + leafName; //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a potential non-null root node, this method looks for the currently edited
+ * file path and uses it as a key to retrieve the last used item for this file by this
+ * selection dialog. Returns null if nothing can be found, otherwise returns the string
+ * name of the item.
+ */
+ private String getLastUsedXmlName(UiElementNode ui_node) {
+ if (ui_node != null) {
+ AndroidXmlEditor editor = ui_node.getEditor();
+ if (editor != null) {
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ IFile f = ((FileEditorInput) editorInput).getFile();
+ if (f != null) {
+ mLastUsedKey = f.getFullPath().toPortableString();
+
+ return sLastUsedXmlName.get(mLastUsedKey);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the last used item for this selection dialog for this file.
+ * @param objects The currently selected items. Only the first one is used if it is an
+ * {@link ElementDescriptor}.
+ */
+ private void setLastUsedXmlName(Object[] objects) {
+ if (mLastUsedKey != null &&
+ objects != null &&
+ objects.length > 0 &&
+ objects[0] instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) objects[0];
+ sLastUsedXmlName.put(mLastUsedKey, desc.getXmlName());
+ }
+ }
+
+ /**
+ * Returns the most used sub-element name, if any, or null.
+ */
+ private String getMostUsedXmlName(UiElementNode ui_node) {
+ if (ui_node != null) {
+ TreeMap<String, Integer> counts = new TreeMap<String, Integer>();
+ int max = -1;
+
+ for (UiElementNode child : ui_node.getUiChildren()) {
+ String name = child.getDescriptor().getXmlName();
+ Integer i = counts.get(name);
+ int count = i == null ? 1 : i.intValue() + 1;
+ counts.put(name, count);
+ max = Math.max(max, count);
+ }
+
+ if (max > 0) {
+ // Find first key with this max and return it
+ for (Entry<String, Integer> entry : counts.entrySet()) {
+ if (entry.getValue().intValue() == max) {
+ return entry.getKey();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The root node selected by the user, either root node or the
+ * one passed to the constructor if not null.
+ */
+ public UiElementNode getChosenRootNode() {
+ return mChosenRootNode;
+ }
+
+ /**
+ * Internal helper to compute the result. Returns the selection from
+ * the list view, if any.
+ */
+ @Override
+ protected void computeResult() {
+ setResult(Arrays.asList(getSelectedElements()));
+ setLastUsedXmlName(getSelectedElements());
+ }
+
+ /**
+ * Creates the dialog area.
+ *
+ * First add a radio area, which may be either 2 radio controls or
+ * just a message area if there's only one choice (the app root node).
+ *
+ * Then uses the default from the AbstractElementListSelectionDialog
+ * which is to add both a filter text and a filtered list. Adding both
+ * is necessary (since the base class accesses both internal directly
+ * fields without checking for null pointers.)
+ *
+ * Finally sets the initial selection list.
+ */
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite contents = (Composite) super.createDialogArea(parent);
+
+ createRadioControl(contents);
+ createFilterText(contents);
+ createFilteredList(contents);
+
+ // We don't want the builtin message area label (we use a radio control
+ // instead), but if we don't create it, Bad Stuff happens on
+ // Eclipse 3.8 and later (see issue 32527).
+ Label label = createMessageArea(contents);
+ if (label != null) {
+ GridData data = (GridData) label.getLayoutData();
+ data.exclude = true;
+ }
+
+ // Initialize the list state.
+ // This must be done after the filtered list as been created.
+ chooseNode(mChosenRootNode);
+
+ // Set the initial selection
+ setInitialSelection(mChosenRootNode);
+ return contents;
+ }
+
+ /**
+ * Tries to set the initial selection based on the {@link #mInitialXmlName} computed
+ * in the constructor. The selection is only set if there's an element descriptor
+ * that matches the same exact XML name. When {@link #mInitialXmlName} starts with an
+ * asterisk, it means to do a partial case-insensitive match on the start of the
+ * strings.
+ */
+ private void setInitialSelection(UiElementNode rootNode) {
+ ElementDescriptor initialElement = null;
+
+ if (mInitialXmlName != null && mInitialXmlName.length() > 0) {
+ String name = mInitialXmlName;
+ boolean partial = name.startsWith("*"); //$NON-NLS-1$
+ if (partial) {
+ name = name.substring(1).toLowerCase(Locale.US);
+ }
+
+ for (ElementDescriptor desc : getAllowedDescriptors(rootNode)) {
+ if (!partial && desc.getXmlName().equals(name)) {
+ initialElement = desc;
+ break;
+ } else if (partial) {
+ String name2 = desc.getXmlLocalName().toLowerCase(Locale.US);
+ if (name.startsWith(name2) || name2.startsWith(name)) {
+ initialElement = desc;
+ break;
+ }
+ }
+ }
+ }
+
+ setSelection(initialElement == null ? null : new ElementDescriptor[] { initialElement });
+ }
+
+ /**
+ * Creates the message text widget and sets layout data.
+ * @param content the parent composite of the message area.
+ */
+ private Composite createRadioControl(Composite content) {
+
+ if (mSelectedUiNode != null) {
+ Button radio1 = new Button(content, SWT.RADIO);
+ radio1.setText(String.format("Create a new element at the top level, in %1$s.",
+ mLocalRootNode.getShortDescription()));
+
+ Button radio2 = new Button(content, SWT.RADIO);
+ radio2.setText(String.format("Create a new element in the selected element, %1$s.",
+ mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */)));
+
+ // Set the initial selection before adding the listeners
+ // (they can't be run till the filtered list has been created)
+ radio1.setSelection(false);
+ radio2.setSelection(true);
+ mChosenRootNode = mSelectedUiNode;
+
+ radio1.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ chooseNode(mLocalRootNode);
+ }
+ });
+
+ radio2.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ chooseNode(mSelectedUiNode);
+ }
+ });
+ } else {
+ setMessage(String.format("Create a new element at the top level, in %1$s.",
+ mLocalRootNode.getShortDescription()));
+ createMessageArea(content);
+
+ mChosenRootNode = mLocalRootNode;
+ }
+
+ return content;
+ }
+
+ /**
+ * Internal helper to remember the root node choosen by the user.
+ * It also sets the list view to the adequate list of children that can
+ * be added to the chosen root node.
+ *
+ * If the chosen root node is mLocalRootNode and a descriptor filter was specified
+ * when creating the master-detail part, we use this as the set of nodes that
+ * can be created on the root node.
+ *
+ * @param ui_node The chosen root node, either mLocalRootNode or
+ * mSelectedUiNode.
+ */
+ private void chooseNode(UiElementNode ui_node) {
+ mChosenRootNode = ui_node;
+ setListElements(getAllowedDescriptors(ui_node));
+ }
+
+ /**
+ * Returns the list of {@link ElementDescriptor}s that can be added to the given
+ * UI node.
+ *
+ * @param ui_node The UI node to which element should be added. Cannot be null.
+ * @return A non-null array of {@link ElementDescriptor}. The array might be empty.
+ */
+ private ElementDescriptor[] getAllowedDescriptors(UiElementNode ui_node) {
+ if (ui_node == mLocalRootNode &&
+ mDescriptorFilters != null &&
+ mDescriptorFilters.length != 0) {
+ return mDescriptorFilters;
+ } else {
+ return ui_node.getDescriptor().getChildren();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java
new file mode 100644
index 000000000..6674ba9ca
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.w3c.dom.Node;
+
+
+/**
+ * Provides Paste operation for the tree nodes
+ */
+@SuppressWarnings("restriction")
+public class PasteAction extends Action {
+ private UiElementNode mUiNode;
+ private final AndroidXmlEditor mEditor;
+ private final Clipboard mClipboard;
+
+ public PasteAction(AndroidXmlEditor editor, Clipboard clipboard, UiElementNode ui_node) {
+ super("Paste");
+ mEditor = editor;
+ mClipboard = clipboard;
+
+ ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE_DISABLED));
+
+ mUiNode = ui_node;
+ }
+
+ /**
+ * Performs the paste operation.
+ */
+ @Override
+ public void run() {
+ super.run();
+
+ final String data = (String) mClipboard.getContents(TextTransfer.getInstance());
+ if (data != null) {
+ mEditor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ IStructuredDocument sse_doc = mEditor.getStructuredDocument();
+ if (sse_doc != null) {
+ if (mUiNode.getDescriptor().hasChildren()) {
+ // This UI Node can have children. The new XML is
+ // inserted as the first child.
+
+ if (mUiNode.getUiChildren().size() > 0) {
+ // There's already at least one child,
+ // so insert right before it.
+ Node xml_node = mUiNode.getUiChildren().get(0).getXmlNode();
+
+ if (xml_node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xml_node;
+ sse_doc.replace(region.getStartOffset(), 0, data);
+ return; // we're done, no need to try the other cases
+ }
+ }
+
+ // If there's no first XML node child. Create one by
+ // inserting at the end of the *start* tag.
+ Node xml_node = mUiNode.getXmlNode();
+ if (xml_node instanceof NodeContainer) {
+ NodeContainer container = (NodeContainer) xml_node;
+ IStructuredDocumentRegion start_tag =
+ container.getStartStructuredDocumentRegion();
+ if (start_tag != null) {
+ sse_doc.replace(start_tag.getEndOffset(), 0, data);
+ return; // we're done, no need to try the other case
+ }
+ }
+ }
+
+ // This UI Node doesn't accept children. The new XML is inserted as the
+ // next sibling. This also serves as a fallback if all the previous
+ // attempts failed. However, this is not possible if the current node
+ // has for parent a document -- an XML document can only have one root,
+ // with no siblings.
+ if (!(mUiNode.getUiParent() instanceof UiDocumentNode)) {
+ Node xml_node = mUiNode.getXmlNode();
+ if (xml_node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xml_node;
+ sse_doc.replace(region.getEndOffset(), 0, data);
+ }
+ }
+ }
+
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e,
+ "ParseAction failed for UI Node %2$s, content '%1$s'", //$NON-NLS-1$
+ mUiNode.getBreadcrumbTrailDescription(true), data);
+ }
+ }
+ });
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java
new file mode 100644
index 000000000..92ccf2e7d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.widgets.Shell;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Performs basic actions on an XML tree: add node, remove node, move up/down.
+ */
+public abstract class UiActions implements ICommitXml {
+
+ public UiActions() {
+ }
+
+ //---------------------
+ // Actual implementations must override these to provide specific hooks
+
+ /** Returns the UiDocumentNode for the current model. */
+ abstract protected UiElementNode getRootNode();
+
+ /** Commits pending data before the XML model is modified. */
+ @Override
+ abstract public void commitPendingXmlChanges();
+
+ /**
+ * Utility method to select an outline item based on its model node
+ *
+ * @param uiNode The node to select. Can be null (in which case nothing should happen)
+ */
+ abstract protected void selectUiNode(UiElementNode uiNode);
+
+ //---------------------
+
+ /**
+ * Called when the "Add..." button next to the tree view is selected.
+ * <p/>
+ * This simplified version of doAdd does not support descriptor filters and creates
+ * a new {@link UiModelTreeLabelProvider} for each call.
+ */
+ public void doAdd(UiElementNode uiNode, Shell shell) {
+ doAdd(uiNode, null /* descriptorFilters */, shell, new UiModelTreeLabelProvider());
+ }
+
+ /**
+ * Called when the "Add..." button next to the tree view is selected.
+ *
+ * Displays a selection dialog that lets the user select which kind of node
+ * to create, depending on the current selection.
+ */
+ public void doAdd(UiElementNode uiNode,
+ ElementDescriptor[] descriptorFilters,
+ Shell shell, ILabelProvider labelProvider) {
+ // If the root node is a document with already a root, use it as the root node
+ UiElementNode rootNode = getRootNode();
+ if (rootNode instanceof UiDocumentNode && rootNode.getUiChildren().size() > 0) {
+ rootNode = rootNode.getUiChildren().get(0);
+ }
+
+ NewItemSelectionDialog dlg = new NewItemSelectionDialog(
+ shell,
+ labelProvider,
+ descriptorFilters,
+ uiNode, rootNode);
+ dlg.open();
+ Object[] results = dlg.getResult();
+ if (results != null && results.length > 0) {
+ addElement(dlg.getChosenRootNode(), null, (ElementDescriptor) results[0],
+ true /*updateLayout*/);
+ }
+ }
+
+ /**
+ * Adds a new XML element based on the {@link ElementDescriptor} to the given parent
+ * {@link UiElementNode}, and then select it.
+ * <p/>
+ * If the parent is a document root which already contains a root element, the inner
+ * root element is used as the actual parent. This ensure you can't create a broken
+ * XML file with more than one root element.
+ * <p/>
+ * If a sibling is given and that sibling has the same parent, the new node is added
+ * right after that sibling. Otherwise the new node is added at the end of the parent
+ * child list.
+ *
+ * @param uiParent An existing UI node or null to add to the tree root
+ * @param uiSibling An existing UI node before which to insert the new node. Can be null.
+ * @param descriptor The descriptor of the element to add
+ * @param updateLayout True if layout attributes should be set
+ * @return The new {@link UiElementNode} or null.
+ */
+ public UiElementNode addElement(UiElementNode uiParent,
+ UiElementNode uiSibling,
+ ElementDescriptor descriptor,
+ boolean updateLayout) {
+ if (uiParent instanceof UiDocumentNode && uiParent.getUiChildren().size() > 0) {
+ uiParent = uiParent.getUiChildren().get(0);
+ }
+ if (uiSibling != null && uiSibling.getUiParent() != uiParent) {
+ uiSibling = null;
+ }
+
+ UiElementNode uiNew = addNewTreeElement(uiParent, uiSibling, descriptor, updateLayout);
+ selectUiNode(uiNew);
+
+ return uiNew;
+ }
+
+ /**
+ * Called when the "Remove" button is selected.
+ *
+ * If the tree has a selection, remove it.
+ * This simply deletes the XML node attached to the UI node: when the XML model fires the
+ * update event, the tree will get refreshed.
+ */
+ public void doRemove(final List<UiElementNode> nodes, Shell shell) {
+
+ if (nodes == null || nodes.size() == 0) {
+ return;
+ }
+
+ final int len = nodes.size();
+
+ StringBuilder sb = new StringBuilder();
+ for (UiElementNode node : nodes) {
+ sb.append("\n- "); //$NON-NLS-1$
+ sb.append(node.getBreadcrumbTrailDescription(false /* include_root */));
+ }
+
+ if (MessageDialog.openQuestion(shell,
+ len > 1 ? "Remove elements from Android XML" // title
+ : "Remove element from Android XML",
+ String.format("Do you really want to remove %1$s?", sb.toString()))) {
+ commitPendingXmlChanges();
+ getRootNode().getEditor().wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ UiElementNode previous = null;
+ UiElementNode parent = null;
+
+ for (int i = len - 1; i >= 0; i--) {
+ UiElementNode node = nodes.get(i);
+ previous = node.getUiPreviousSibling();
+ parent = node.getUiParent();
+
+ // delete node
+ node.deleteXmlNode();
+ }
+
+ // try to select the last previous sibling or the last parent
+ if (previous != null) {
+ selectUiNode(previous);
+ } else if (parent != null) {
+ selectUiNode(parent);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Called when the "Up" button is selected.
+ * <p/>
+ * If the tree has a selection, move it up, either in the child list or as the last child
+ * of the previous parent.
+ */
+ public void doUp(
+ final List<UiElementNode> uiNodes,
+ final ElementDescriptor[] descriptorFilters) {
+ if (uiNodes == null || uiNodes.size() < 1) {
+ return;
+ }
+
+ final Node[] selectXmlNode = { null };
+ final UiElementNode[] uiLastNode = { null };
+ final UiElementNode[] uiSearchRoot = { null };
+
+ commitPendingXmlChanges();
+ getRootNode().getEditor().wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < uiNodes.size(); i++) {
+ UiElementNode uiNode = uiLastNode[0] = uiNodes.get(i);
+ doUpInternal(
+ uiNode,
+ descriptorFilters,
+ selectXmlNode,
+ uiSearchRoot,
+ false /*testOnly*/);
+ }
+ }
+ });
+
+ assert uiLastNode[0] != null; // tell Eclipse this can't be null below
+
+ if (selectXmlNode[0] == null) {
+ // The XML node has not been moved, we can just select the same UI node
+ selectUiNode(uiLastNode[0]);
+ } else {
+ // The XML node has moved. At this point the UI model has been reloaded
+ // and the XML node has been affected to a new UI node. Find that new UI
+ // node and select it.
+ if (uiSearchRoot[0] == null) {
+ uiSearchRoot[0] = uiLastNode[0].getUiRoot();
+ }
+ if (uiSearchRoot[0] != null) {
+ selectUiNode(uiSearchRoot[0].findXmlNode(selectXmlNode[0]));
+ }
+ }
+ }
+
+ /**
+ * Checks whether the "up" action can be performed on all items.
+ *
+ * @return True if the up action can be carried on *all* items.
+ */
+ public boolean canDoUp(
+ List<UiElementNode> uiNodes,
+ ElementDescriptor[] descriptorFilters) {
+ if (uiNodes == null || uiNodes.size() < 1) {
+ return false;
+ }
+
+ final Node[] selectXmlNode = { null };
+ final UiElementNode[] uiSearchRoot = { null };
+
+ commitPendingXmlChanges();
+
+ for (int i = 0; i < uiNodes.size(); i++) {
+ if (!doUpInternal(
+ uiNodes.get(i),
+ descriptorFilters,
+ selectXmlNode,
+ uiSearchRoot,
+ true /*testOnly*/)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean doUpInternal(
+ UiElementNode uiNode,
+ ElementDescriptor[] descriptorFilters,
+ Node[] outSelectXmlNode,
+ UiElementNode[] outUiSearchRoot,
+ boolean testOnly) {
+ // the node will move either up to its parent or grand-parent
+ outUiSearchRoot[0] = uiNode.getUiParent();
+ if (outUiSearchRoot[0] != null && outUiSearchRoot[0].getUiParent() != null) {
+ outUiSearchRoot[0] = outUiSearchRoot[0].getUiParent();
+ }
+ Node xmlNode = uiNode.getXmlNode();
+ ElementDescriptor nodeDesc = uiNode.getDescriptor();
+ if (xmlNode == null || nodeDesc == null) {
+ return false;
+ }
+ UiElementNode uiParentNode = uiNode.getUiParent();
+ Node xmlParent = uiParentNode == null ? null : uiParentNode.getXmlNode();
+ if (xmlParent == null) {
+ return false;
+ }
+
+ UiElementNode uiPrev = uiNode.getUiPreviousSibling();
+
+ // Only accept a sibling that has an XML attached and
+ // is part of the allowed descriptor filters.
+ while (uiPrev != null &&
+ (uiPrev.getXmlNode() == null || !matchDescFilter(descriptorFilters, uiPrev))) {
+ uiPrev = uiPrev.getUiPreviousSibling();
+ }
+
+ if (uiPrev != null && uiPrev.getXmlNode() != null) {
+ // This node is not the first one of the parent.
+ Node xmlPrev = uiPrev.getXmlNode();
+ if (uiPrev.getDescriptor().acceptChild(nodeDesc)) {
+ // If the previous sibling can accept this child, then it
+ // is inserted at the end of the children list.
+ if (testOnly) {
+ return true;
+ }
+ xmlPrev.appendChild(xmlParent.removeChild(xmlNode));
+ outSelectXmlNode[0] = xmlNode;
+ } else {
+ // This node is not the first one of the parent, so it can be
+ // removed and then inserted before its previous sibling.
+ if (testOnly) {
+ return true;
+ }
+ xmlParent.insertBefore(
+ xmlParent.removeChild(xmlNode),
+ xmlPrev);
+ outSelectXmlNode[0] = xmlNode;
+ }
+ } else if (uiParentNode != null && !(xmlParent instanceof Document)) {
+ UiElementNode uiGrandParent = uiParentNode.getUiParent();
+ Node xmlGrandParent = uiGrandParent == null ? null : uiGrandParent.getXmlNode();
+ ElementDescriptor grandDesc =
+ uiGrandParent == null ? null : uiGrandParent.getDescriptor();
+
+ if (xmlGrandParent != null &&
+ !(xmlGrandParent instanceof Document) &&
+ grandDesc != null &&
+ grandDesc.acceptChild(nodeDesc)) {
+ // If the node is the first one of the child list of its
+ // parent, move it up in the hierarchy as previous sibling
+ // to the parent. This is only possible if the parent of the
+ // parent is not a document.
+ // The parent node must actually accept this kind of child.
+
+ if (testOnly) {
+ return true;
+ }
+ xmlGrandParent.insertBefore(
+ xmlParent.removeChild(xmlNode),
+ xmlParent);
+ outSelectXmlNode[0] = xmlNode;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean matchDescFilter(
+ ElementDescriptor[] descriptorFilters,
+ UiElementNode uiNode) {
+ if (descriptorFilters == null || descriptorFilters.length < 1) {
+ return true;
+ }
+
+ ElementDescriptor desc = uiNode.getDescriptor();
+
+ for (ElementDescriptor filter : descriptorFilters) {
+ if (filter.equals(desc)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called when the "Down" button is selected.
+ *
+ * If the tree has a selection, move it down, either in the same child list or as the
+ * first child of the next parent.
+ */
+ public void doDown(
+ final List<UiElementNode> nodes,
+ final ElementDescriptor[] descriptorFilters) {
+ if (nodes == null || nodes.size() < 1) {
+ return;
+ }
+
+ final Node[] selectXmlNode = { null };
+ final UiElementNode[] uiLastNode = { null };
+ final UiElementNode[] uiSearchRoot = { null };
+
+ commitPendingXmlChanges();
+ getRootNode().getEditor().wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = nodes.size() - 1; i >= 0; i--) {
+ final UiElementNode node = uiLastNode[0] = nodes.get(i);
+ doDownInternal(
+ node,
+ descriptorFilters,
+ selectXmlNode,
+ uiSearchRoot,
+ false /*testOnly*/);
+ }
+ }
+ });
+
+ assert uiLastNode[0] != null; // tell Eclipse this can't be null below
+
+ if (selectXmlNode[0] == null) {
+ // The XML node has not been moved, we can just select the same UI node
+ selectUiNode(uiLastNode[0]);
+ } else {
+ // The XML node has moved. At this point the UI model has been reloaded
+ // and the XML node has been affected to a new UI node. Find that new UI
+ // node and select it.
+ if (uiSearchRoot[0] == null) {
+ uiSearchRoot[0] = uiLastNode[0].getUiRoot();
+ }
+ if (uiSearchRoot[0] != null) {
+ selectUiNode(uiSearchRoot[0].findXmlNode(selectXmlNode[0]));
+ }
+ }
+ }
+
+ /**
+ * Checks whether the "down" action can be performed on all items.
+ *
+ * @return True if the down action can be carried on *all* items.
+ */
+ public boolean canDoDown(
+ List<UiElementNode> uiNodes,
+ ElementDescriptor[] descriptorFilters) {
+ if (uiNodes == null || uiNodes.size() < 1) {
+ return false;
+ }
+
+ final Node[] selectXmlNode = { null };
+ final UiElementNode[] uiSearchRoot = { null };
+
+ commitPendingXmlChanges();
+
+ for (int i = 0; i < uiNodes.size(); i++) {
+ if (!doDownInternal(
+ uiNodes.get(i),
+ descriptorFilters,
+ selectXmlNode,
+ uiSearchRoot,
+ true /*testOnly*/)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean doDownInternal(
+ UiElementNode uiNode,
+ ElementDescriptor[] descriptorFilters,
+ Node[] outSelectXmlNode,
+ UiElementNode[] outUiSearchRoot,
+ boolean testOnly) {
+ // the node will move either down to its parent or grand-parent
+ outUiSearchRoot[0] = uiNode.getUiParent();
+ if (outUiSearchRoot[0] != null && outUiSearchRoot[0].getUiParent() != null) {
+ outUiSearchRoot[0] = outUiSearchRoot[0].getUiParent();
+ }
+
+ Node xmlNode = uiNode.getXmlNode();
+ ElementDescriptor nodeDesc = uiNode.getDescriptor();
+ if (xmlNode == null || nodeDesc == null) {
+ return false;
+ }
+ UiElementNode uiParentNode = uiNode.getUiParent();
+ Node xmlParent = uiParentNode == null ? null : uiParentNode.getXmlNode();
+ if (xmlParent == null) {
+ return false;
+ }
+
+ UiElementNode uiNext = uiNode.getUiNextSibling();
+
+ // Only accept a sibling that has an XML attached and
+ // is part of the allowed descriptor filters.
+ while (uiNext != null &&
+ (uiNext.getXmlNode() == null || !matchDescFilter(descriptorFilters, uiNext))) {
+ uiNext = uiNext.getUiNextSibling();
+ }
+
+ if (uiNext != null && uiNext.getXmlNode() != null) {
+ // This node is not the last one of the parent.
+ Node xmlNext = uiNext.getXmlNode();
+ // If the next sibling is a node that can have children, though,
+ // then the node is inserted as the first child.
+ if (uiNext.getDescriptor().acceptChild(nodeDesc)) {
+ if (testOnly) {
+ return true;
+ }
+ // Note: insertBefore works as append if the ref node is
+ // null, i.e. when the node doesn't have children yet.
+ xmlNext.insertBefore(
+ xmlParent.removeChild(xmlNode),
+ xmlNext.getFirstChild());
+ outSelectXmlNode[0] = xmlNode;
+ } else {
+ // This node is not the last one of the parent, so it can be
+ // removed and then inserted after its next sibling.
+
+ if (testOnly) {
+ return true;
+ }
+ // Insert "before after next" ;-)
+ xmlParent.insertBefore(
+ xmlParent.removeChild(xmlNode),
+ xmlNext.getNextSibling());
+ outSelectXmlNode[0] = xmlNode;
+ }
+ } else if (uiParentNode != null && !(xmlParent instanceof Document)) {
+ UiElementNode uiGrandParent = uiParentNode.getUiParent();
+ Node xmlGrandParent = uiGrandParent == null ? null : uiGrandParent.getXmlNode();
+ ElementDescriptor grandDesc =
+ uiGrandParent == null ? null : uiGrandParent.getDescriptor();
+
+ if (xmlGrandParent != null &&
+ !(xmlGrandParent instanceof Document) &&
+ grandDesc != null &&
+ grandDesc.acceptChild(nodeDesc)) {
+ // This node is the last node of its parent.
+ // If neither the parent nor the grandparent is a document,
+ // then the node can be inserted right after the parent.
+ // The parent node must actually accept this kind of child.
+ if (testOnly) {
+ return true;
+ }
+ xmlGrandParent.insertBefore(
+ xmlParent.removeChild(xmlNode),
+ xmlParent.getNextSibling());
+ outSelectXmlNode[0] = xmlNode;
+ }
+ }
+
+ return false;
+ }
+
+ //---------------------
+
+ /**
+ * Adds a new element of the given descriptor's type to the given UI parent node.
+ *
+ * This actually creates the corresponding XML node in the XML model, which in turn
+ * will refresh the current tree view.
+ *
+ * @param uiParent An existing UI node or null to add to the tree root
+ * @param uiSibling An existing UI node to insert right before. Can be null.
+ * @param descriptor The descriptor of the element to add
+ * @param updateLayout True if layout attributes should be set
+ * @return The {@link UiElementNode} that has been added to the UI tree.
+ */
+ private UiElementNode addNewTreeElement(UiElementNode uiParent,
+ UiElementNode uiSibling,
+ ElementDescriptor descriptor,
+ final boolean updateLayout) {
+ commitPendingXmlChanges();
+
+ List<UiElementNode> uiChildren = uiParent.getUiChildren();
+ int n = uiChildren.size();
+
+ // The default is to append at the end of the list.
+ int index = n;
+
+ if (uiSibling != null) {
+ // Try to find the requested sibling.
+ index = uiChildren.indexOf(uiSibling);
+ if (index < 0) {
+ // This sibling didn't exist. Should not happen but compensate
+ // by simply adding to the end of the list.
+ uiSibling = null;
+ index = n;
+ }
+ }
+
+ if (uiSibling == null) {
+ // If we don't require any specific position, make sure to insert before the
+ // first mandatory_last descriptor's position, if any.
+
+ for (int i = 0; i < n; i++) {
+ UiElementNode uiChild = uiChildren.get(i);
+ if (uiChild.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST) {
+ index = i;
+ break;
+ }
+ }
+ }
+
+ final UiElementNode uiNew = uiParent.insertNewUiChild(index, descriptor);
+ UiElementNode rootNode = getRootNode();
+
+ rootNode.getEditor().wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, updateLayout);
+ uiNew.createXmlNode();
+ }
+ });
+ return uiNew;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java
new file mode 100644
index 000000000..2aa56a826
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IDetailsPage;
+import org.eclipse.ui.forms.IFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.ExpansionEvent;
+import org.eclipse.ui.forms.events.IExpansionListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+/**
+ * Details page for the {@link UiElementNode} nodes in the tree view.
+ * <p/>
+ * See IDetailsBase for more details.
+ */
+class UiElementDetail implements IDetailsPage {
+
+ /** The master-detail part, composed of a main tree and an auxiliary detail part */
+ private ManifestSectionPart mMasterPart;
+
+ private Section mMasterSection;
+ private UiElementNode mCurrentUiElementNode;
+ private Composite mCurrentTable;
+ private boolean mIsDirty;
+
+ private IManagedForm mManagedForm;
+
+ private final UiTreeBlock mTree;
+
+ public UiElementDetail(UiTreeBlock tree) {
+ mTree = tree;
+ mMasterPart = mTree.getMasterPart();
+ mManagedForm = mMasterPart.getManagedForm();
+ }
+
+ /* (non-java doc)
+ * Initializes the part.
+ */
+ @Override
+ public void initialize(IManagedForm form) {
+ mManagedForm = form;
+ }
+
+ /* (non-java doc)
+ * Creates the contents of the page in the provided parent.
+ */
+ @Override
+ public void createContents(Composite parent) {
+ mMasterSection = createMasterSection(parent);
+ }
+
+ /* (non-java doc)
+ * Called when the provided part has changed selection state.
+ * <p/>
+ * Only reply when our master part originates the selection.
+ */
+ @Override
+ public void selectionChanged(IFormPart part, ISelection selection) {
+ if (part == mMasterPart &&
+ !selection.isEmpty() &&
+ selection instanceof ITreeSelection) {
+ ITreeSelection tree_selection = (ITreeSelection) selection;
+
+ Object first = tree_selection.getFirstElement();
+ if (first instanceof UiElementNode) {
+ UiElementNode ui_node = (UiElementNode) first;
+ createUiAttributeControls(mManagedForm, ui_node);
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Instructs it to commit the new (modified) data back into the model.
+ */
+ @Override
+ public void commit(boolean onSave) {
+
+ mTree.getEditor().wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (mCurrentUiElementNode != null) {
+ mCurrentUiElementNode.commit();
+ }
+
+ // Finally reset the dirty flag if everything was saved properly
+ mIsDirty = false;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$
+ }
+ }
+ });
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+
+ /* (non-java doc)
+ * Returns true if the part has been modified with respect to the data
+ * loaded from the model.
+ */
+ @Override
+ public boolean isDirty() {
+ if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) {
+ markDirty();
+ }
+ return mIsDirty;
+ }
+
+ @Override
+ public boolean isStale() {
+ // pass
+ return false;
+ }
+
+ /**
+ * Called by the master part when the tree is refreshed after the framework resources
+ * have been reloaded.
+ */
+ @Override
+ public void refresh() {
+ if (mCurrentTable != null) {
+ mCurrentTable.dispose();
+ mCurrentTable = null;
+ }
+ mCurrentUiElementNode = null;
+ mMasterSection.getParent().pack(true /* changed */);
+ }
+
+ @Override
+ public void setFocus() {
+ // pass
+ }
+
+ @Override
+ public boolean setFormInput(Object input) {
+ // pass
+ return false;
+ }
+
+ /**
+ * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section.
+ *
+ * All the UI should be created in a layout which parent is the mSection itself.
+ * The hierarchy is:
+ * <pre>
+ * DetailPage
+ * + TableWrapLayout
+ * + Section (with title/description && fill_grab horizontal)
+ * + TableWrapLayout [*]
+ * + Labels/Forms/etc... [*]
+ * </pre>
+ * Both items marked with [*] are created by the derived classes to fit their needs.
+ *
+ * @param parent Parent of the mSection (from createContents)
+ * @return The new Section
+ */
+ private Section createMasterSection(Composite parent) {
+ TableWrapLayout layout = new TableWrapLayout();
+ layout.topMargin = 0;
+ parent.setLayout(layout);
+
+ FormToolkit toolkit = mManagedForm.getToolkit();
+ Section section = toolkit.createSection(parent, Section.TITLE_BAR);
+ section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
+ return section;
+ }
+
+ /**
+ * Create the ui attribute controls to edit the attributes for the given
+ * ElementDescriptor.
+ * <p/>
+ * This is called by the constructor.
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The managed form
+ */
+ private void createUiAttributeControls(
+ final IManagedForm managedForm,
+ final UiElementNode ui_node) {
+
+ final ElementDescriptor elem_desc = ui_node.getDescriptor();
+ mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription()));
+
+ if (mCurrentUiElementNode != ui_node) {
+ // Before changing the table, commit all dirty state.
+ if (mIsDirty) {
+ commit(false);
+ }
+ if (mCurrentTable != null) {
+ mCurrentTable.dispose();
+ mCurrentTable = null;
+ }
+
+ // To iterate over all attributes, we use the {@link ElementDescriptor} instead
+ // of the {@link UiElementNode} because the attributes order is guaranteed in the
+ // descriptor but not in the node itself.
+ AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors();
+
+ // If the attribute list contains at least one SeparatorAttributeDescriptor,
+ // sub-sections will be used. This needs to be known early as it influences the
+ // creation of the master table.
+ boolean useSubsections = false;
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ // Sub-sections will be used. The default sections should no longer be
+ useSubsections = true;
+ break;
+ }
+ }
+
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite masterTable = SectionHelper.createTableLayout(mMasterSection,
+ toolkit, useSubsections ? 1 : 2 /* numColumns */);
+ mCurrentTable = masterTable;
+
+ mCurrentUiElementNode = ui_node;
+
+ if (elem_desc.getTooltip() != null) {
+ String tooltip;
+ if (Sdk.getCurrent() != null &&
+ Sdk.getCurrent().getDocumentationBaseUrl() != null) {
+ tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(),
+ elem_desc,
+ Sdk.getCurrent().getDocumentationBaseUrl());
+ } else {
+ tooltip = elem_desc.getTooltip();
+ }
+
+ try {
+ FormText text = SectionHelper.createFormText(masterTable, toolkit,
+ true /* isHtml */, tooltip, true /* setupLayoutData */);
+ text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener());
+ Image icon = elem_desc.getCustomizedIcon();
+ if (icon != null) {
+ text.setImage(DescriptorsUtils.IMAGE_KEY, icon);
+ }
+ } catch(Exception e) {
+ // The FormText parser is really really basic and will fail as soon as the
+ // HTML javadoc is ever so slightly malformatted.
+ AdtPlugin.log(e,
+ "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$
+ ui_node.getDescriptor().getXmlName(),
+ tooltip);
+
+ // Fallback to a pure text tooltip, no fancy HTML
+ tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip());
+ SectionHelper.createLabel(masterTable, toolkit, tooltip, tooltip);
+ }
+ }
+
+ Composite table = useSubsections ? null : masterTable;
+
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) {
+ String title = null;
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ // xmlName is actually the label of the separator
+ title = attr_desc.getXmlLocalName();
+ } else {
+ title = String.format("Attributes from %1$s", elem_desc.getUiName());
+ }
+
+ table = createSubSectionTable(toolkit, masterTable, title);
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ continue;
+ }
+ }
+
+ UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc);
+
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+
+ if (ui_attr.getCurrentValue() != null &&
+ ui_attr.getCurrentValue().length() > 0) {
+ ((Section) table.getParent()).setExpanded(true);
+ }
+ } else {
+ // The XML has an extra unknown attribute.
+ // This is not expected to happen so it is ignored.
+ AdtPlugin.log(IStatus.INFO,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ ui_node.getDescriptor().getXmlName());
+ }
+ }
+
+ // Create a sub-section for the unknown attributes.
+ // It is initially hidden till there are some attributes to show here.
+ final Composite unknownTable = createSubSectionTable(toolkit, masterTable,
+ "Unknown XML Attributes");
+ unknownTable.getParent().setVisible(false); // set section to not visible
+ final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>();
+
+ final IUiUpdateListener updateListener = new IUiUpdateListener() {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode uiNode, UiUpdateState state) {
+ if (state == UiUpdateState.ATTR_UPDATED) {
+ updateUnknownAttributesSection(uiNode, unknownTable, managedForm,
+ reference);
+ }
+ }
+ };
+ ui_node.addUpdateListener(updateListener);
+
+ // remove the listener when the UI is disposed
+ unknownTable.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ ui_node.removeUpdateListener(updateListener);
+ }
+ });
+
+ updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference);
+
+ mMasterSection.getParent().pack(true /* changed */);
+ }
+ }
+
+ /**
+ * Create a sub Section and its embedding wrapper table with 2 columns.
+ * @return The table, child of a new section.
+ */
+ private Composite createSubSectionTable(FormToolkit toolkit,
+ Composite masterTable, String title) {
+
+ // The Section composite seems to ignore colspan when assigned a TableWrapData so
+ // if the parent is a table with more than one column an extra table with one column
+ // is inserted to respect colspan.
+ int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns;
+ if (parentNumCol > 1) {
+ masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1);
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ twd.colspan = parentNumCol;
+ masterTable.setLayoutData(twd);
+ }
+
+ Composite table;
+ Section section = toolkit.createSection(masterTable,
+ Section.TITLE_BAR | Section.TWISTIE);
+
+ // Add an expansion listener that will trigger a reflow on the parent
+ // ScrolledPageBook (which is actually a SharedScrolledComposite). This will
+ // recompute the correct size and adjust the scrollbar as needed.
+ section.addExpansionListener(new IExpansionListener() {
+ @Override
+ public void expansionStateChanged(ExpansionEvent e) {
+ reflowMasterSection();
+ }
+
+ @Override
+ public void expansionStateChanging(ExpansionEvent e) {
+ // pass
+ }
+ });
+
+ section.setText(title);
+ section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB,
+ TableWrapData.TOP));
+ table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */);
+ return table;
+ }
+
+ /**
+ * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite).
+ * This will recompute the correct size and adjust the scrollbar as needed.
+ */
+ private void reflowMasterSection() {
+ for(Composite c = mMasterSection; c != null; c = c.getParent()) {
+ if (c instanceof SharedScrolledComposite) {
+ ((SharedScrolledComposite) c).reflow(true /* flushCache */);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the unknown attributes section for the UI Node.
+ */
+ private void updateUnknownAttributesSection(UiElementNode ui_node,
+ final Composite unknownTable, final IManagedForm managedForm,
+ HashSet<UiAttributeNode> reference) {
+ Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes();
+ Section section = ((Section) unknownTable.getParent());
+ boolean needs_reflow = false;
+
+ // The table was created hidden, show it if there are unknown attributes now
+ if (ui_attrs.size() > 0 && !section.isVisible()) {
+ section.setVisible(true);
+ needs_reflow = true;
+ }
+
+ // Compare the new attribute set with the old "reference" one
+ boolean has_differences = ui_attrs.size() != reference.size();
+ if (!has_differences) {
+ for (UiAttributeNode ui_attr : ui_attrs) {
+ if (!reference.contains(ui_attr)) {
+ has_differences = true;
+ break;
+ }
+ }
+ }
+
+ if (has_differences) {
+ needs_reflow = true;
+ reference.clear();
+
+ // Remove all children of the table
+ for (Control c : unknownTable.getChildren()) {
+ c.dispose();
+ }
+
+ // Recreate all attributes UI
+ for (UiAttributeNode ui_attr : ui_attrs) {
+ reference.add(ui_attr);
+ ui_attr.createUiControl(unknownTable, managedForm);
+
+ if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) {
+ section.setExpanded(true);
+ }
+ }
+ }
+
+ if (needs_reflow) {
+ reflowMasterSection();
+ }
+ }
+
+ /**
+ * Marks the part dirty. Called as a result of user interaction with the widgets in the
+ * section.
+ */
+ private void markDirty() {
+ if (!mIsDirty) {
+ mIsDirty = true;
+ mManagedForm.dirtyStateChanged();
+ }
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java
new file mode 100644
index 000000000..14049cf86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.ArrayList;
+
+/**
+ * UiModelTreeContentProvider is a trivial implementation of {@link ITreeContentProvider}
+ * where elements are expected to be instances of {@link UiElementNode}.
+ */
+class UiModelTreeContentProvider implements ITreeContentProvider {
+
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The uiRootNode of the model. */
+ private final UiElementNode mUiRootNode;
+
+ public UiModelTreeContentProvider(UiElementNode uiRootNode,
+ ElementDescriptor[] descriptorFilters) {
+ mUiRootNode = uiRootNode;
+ mDescriptorFilters = descriptorFilters;
+ }
+
+ /* (non-java doc)
+ * Returns all the UI node children of the given element or null if not the right kind
+ * of object. */
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) parentElement;
+ return node.getUiChildren().toArray();
+ }
+ return null;
+ }
+
+ /* (non-java doc)
+ * Returns the parent of a given UI node or null if it's a root node or it's not the
+ * right kind of node. */
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getUiParent();
+ }
+ return null;
+ }
+
+ /* (non-java doc)
+ * Returns true if the UI node has any UI children nodes. */
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getUiChildren().size() > 0;
+ }
+ return false;
+ }
+
+ /* (non-java doc)
+ * Get root elements for the tree. These are all the UI nodes that
+ * match the filter descriptor in the current root node.
+ * <p/>
+ * Although not documented, it seems this method should not return null.
+ * At worse, it should return new Object[0].
+ * <p/>
+ * inputElement is not currently used. The root node and the filter are given
+ * by the enclosing class.
+ */
+ @Override
+ public Object[] getElements(Object inputElement) {
+ ArrayList<UiElementNode> roots = new ArrayList<UiElementNode>();
+ if (mUiRootNode != null) {
+ for (UiElementNode ui_node : mUiRootNode.getUiChildren()) {
+ if (mDescriptorFilters == null || mDescriptorFilters.length == 0) {
+ roots.add(ui_node);
+ } else {
+ for (ElementDescriptor filter : mDescriptorFilters) {
+ if (ui_node.getDescriptor() == filter) {
+ roots.add(ui_node);
+ }
+ }
+ }
+ }
+ }
+
+ return roots.toArray();
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java
new file mode 100644
index 000000000..337319761
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * UiModelTreeLabelProvider is a trivial implementation of {@link ILabelProvider}
+ * where elements are expected to derive from {@link UiElementNode} or
+ * from {@link ElementDescriptor}.
+ *
+ * It is used by both the master tree viewer and by the list in the Add... selection dialog.
+ */
+public class UiModelTreeLabelProvider implements ILabelProvider {
+
+ public UiModelTreeLabelProvider() {
+ }
+
+ /**
+ * Returns the element's logo with a fallback on the android logo.
+ */
+ @Override
+ public Image getImage(Object element) {
+ ElementDescriptor desc = null;
+ UiElementNode node = null;
+
+ if (element instanceof ElementDescriptor) {
+ desc = (ElementDescriptor) element;
+ } else if (element instanceof UiElementNode) {
+ node = (UiElementNode) element;
+ desc = node.getDescriptor();
+ }
+
+ if (desc != null) {
+ Image img = desc.getCustomizedIcon();
+ if (img != null) {
+ if (node != null && node.hasError()) {
+ return IconFactory.getInstance().addErrorIcon(img);
+ } else {
+ return img;
+ }
+ }
+ }
+
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Uses UiElementNode.shortDescription for the label for this tree item.
+ */
+ @Override
+ public String getText(Object element) {
+ if (element instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) element;
+ return desc.getUiName();
+ } else if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getShortDescription();
+ }
+ return element.toString();
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java
new file mode 100644
index 000000000..d11b8a4c6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java
@@ -0,0 +1,946 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.forms.DetailsPart;
+import org.eclipse.ui.forms.IDetailsPage;
+import org.eclipse.ui.forms.IDetailsPageProvider;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.MasterDetailsBlock;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for
+ * a specific set of {@link UiElementNode}.
+ * <p/>
+ * For a given UI element node, the tree view displays all first-level children that
+ * match a given type (given by an {@link ElementDescriptor}. All children from these
+ * nodes are also displayed.
+ * <p/>
+ * In the middle next to the tree are some controls to add or delete tree nodes.
+ * On the left is a details part that displays all the visible UI attributes for a given
+ * selected UI element node.
+ */
+public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml {
+
+ /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */
+ private static final int TREE_HEIGHT_HINT = 50;
+
+ /** Container editor */
+ AndroidXmlEditor mEditor;
+ /** The root {@link UiElementNode} which contains all the elements that are to be
+ * manipulated by this tree view. In general this is the manifest UI node. */
+ private UiElementNode mUiRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. Can be null or empty to mean everything
+ * can be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */
+ private String mTitle;
+ /** The description for the master-detail part (displayed on top of the tree view) */
+ private String mDescription;
+ /** The master-detail part, composed of a main tree and an auxiliary detail part */
+ private ManifestSectionPart mMasterPart;
+ /** The tree viewer in the master-detail part */
+ private TreeViewer mTreeViewer;
+ /** The "add" button for the tree view */
+ private Button mAddButton;
+ /** The "remove" button for the tree view */
+ private Button mRemoveButton;
+ /** The "up" button for the tree view */
+ private Button mUpButton;
+ /** The "down" button for the tree view */
+ private Button mDownButton;
+ /** The Managed Form used to create the master part */
+ private IManagedForm mManagedForm;
+ /** Reference to the details part of the tree master block. */
+ private DetailsPart mDetailsPart;
+ /** Reference to the clipboard for copy-paste */
+ private Clipboard mClipboard;
+ /** Listener to refresh the tree viewer when the parent's node has been updated */
+ private IUiUpdateListener mUiRefreshListener;
+ /** Listener to enable/disable the UI based on the application node's presence */
+ private IUiUpdateListener mUiEnableListener;
+ /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */
+ private UiTreeActions mUiTreeActions;
+ /**
+ * True if the root node can be created on-demand (i.e. as needed as
+ * soon as children exist). False if an external entity controls the existence of the
+ * root node. In practise, this is false for the manifest application page (the actual
+ * "application" node is managed by the ApplicationToggle part) whereas it is true
+ * for all other tree pages.
+ */
+ private final boolean mAutoCreateRoot;
+
+
+ /**
+ * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the
+ * given filter in the given root node.
+ *
+ * @param editor The parent manifest editor.
+ * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
+ * to be manipulated by this tree view. In general this is the manifest UI node or the
+ * application UI node. This cannot be null.
+ * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as
+ * soon as children exist). False if an external entity controls the existence of the
+ * root node. In practise, this is false for the manifest application page (the actual
+ * "application" node is managed by the ApplicationToggle part) whereas it is true
+ * for all other tree pages.
+ * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
+ * this tree view. Use null or an empty list to accept any kind of node.
+ * @param title Title for the section
+ * @param description Description for the section
+ */
+ public UiTreeBlock(AndroidXmlEditor editor,
+ UiElementNode uiRootNode,
+ boolean autoCreateRoot,
+ ElementDescriptor[] descriptorFilters,
+ String title,
+ String description) {
+ mEditor = editor;
+ mUiRootNode = uiRootNode;
+ mAutoCreateRoot = autoCreateRoot;
+ mDescriptorFilters = descriptorFilters;
+ mTitle = title;
+ mDescription = description;
+ }
+
+ /** @returns The container editor */
+ AndroidXmlEditor getEditor() {
+ return mEditor;
+ }
+
+ /** @returns The reference to the clipboard for copy-paste */
+ Clipboard getClipboard() {
+ return mClipboard;
+ }
+
+ /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */
+ ManifestSectionPart getMasterPart() {
+ return mMasterPart;
+ }
+
+ /**
+ * Returns the {@link UiElementNode} for the current model.
+ * <p/>
+ * This is used by the content provider attached to {@link #mTreeViewer} since
+ * the uiRootNode changes after each call to
+ * {@link #changeRootAndDescriptors(UiElementNode, ElementDescriptor[], boolean)}.
+ */
+ public UiElementNode getRootNode() {
+ return mUiRootNode;
+ }
+
+ @Override
+ protected void createMasterPart(final IManagedForm managedForm, Composite parent) {
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ mManagedForm = managedForm;
+ mMasterPart = new ManifestSectionPart(parent, toolkit);
+ Section section = mMasterPart.getSection();
+ section.setText(mTitle);
+ section.setDescription(mDescription);
+ section.setLayout(new GridLayout());
+ section.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Composite grid = SectionHelper.createGridLayout(section, toolkit, 2);
+
+ Tree tree = createTreeViewer(toolkit, grid, managedForm);
+ createButtons(toolkit, grid);
+ createTreeContextMenu(tree);
+ createSectionActions(section, toolkit);
+ }
+
+ private void createSectionActions(Section section, FormToolkit toolkit) {
+ ToolBarManager manager = new ToolBarManager(SWT.FLAT);
+ manager.removeAll();
+
+ ToolBar toolbar = manager.createControl(section);
+ section.setTextClient(toolbar);
+
+ ElementDescriptor[] descs = mDescriptorFilters;
+ if (descs == null && mUiRootNode != null) {
+ descs = mUiRootNode.getDescriptor().getChildren();
+ }
+
+ if (descs != null && descs.length > 1) {
+ for (ElementDescriptor desc : descs) {
+ manager.add(new DescriptorFilterAction(desc));
+ }
+ }
+
+ manager.add(new TreeSortAction());
+
+ manager.update(true /*force*/);
+ }
+
+ /**
+ * Creates the tree and its viewer
+ * @return The tree control
+ */
+ private Tree createTreeViewer(FormToolkit toolkit, Composite grid,
+ final IManagedForm managedForm) {
+ // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here.
+ // However the class must be adapted to create an adapted toolkit tree.
+ final Tree tree = toolkit.createTree(grid, SWT.MULTI);
+ GridData gd = new GridData(GridData.FILL_BOTH);
+ gd.widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ gd.heightHint = TREE_HEIGHT_HINT;
+ tree.setLayoutData(gd);
+
+ mTreeViewer = new TreeViewer(tree);
+ mTreeViewer.setContentProvider(new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
+ mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider());
+ mTreeViewer.setInput("unused"); //$NON-NLS-1$
+
+ // Create a listener that reacts to selections on the tree viewer.
+ // When a selection is made, ask the managed form to propagate an event to
+ // all parts in the managed form.
+ // This is picked up by UiElementDetail.selectionChanged().
+ mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ managedForm.fireSelectionChanged(mMasterPart, event.getSelection());
+ adjustTreeButtons(event.getSelection());
+ }
+ });
+
+ // Create three listeners:
+ // - One to refresh the tree viewer when the parent's node has been updated
+ // - One to refresh the tree viewer when the framework resources have changed
+ // - One to enable/disable the UI based on the application node's presence.
+ mUiRefreshListener = new IUiUpdateListener() {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ mTreeViewer.refresh();
+ }
+ };
+
+ mUiEnableListener = new IUiUpdateListener() {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // Normally, we enable the UI here if the XML node is not null.
+ //
+ // However if mAutoCreateRoot is true, the root node will be created on-demand
+ // so the tree/block is always enabled.
+ boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null);
+ if (mMasterPart != null) {
+ Section section = mMasterPart.getSection();
+ if (section.getEnabled() != exists) {
+ section.setEnabled(exists);
+ for (Control c : section.getChildren()) {
+ c.setEnabled(exists);
+ }
+ }
+ }
+ }
+ };
+
+ /** Listener to update the root node if the target of the file is changed because of a
+ * SDK location change or a project target change */
+ final ITargetChangeListener targetListener = new TargetChangeListener() {
+ @Override
+ public IProject getProject() {
+ if (mEditor != null) {
+ return mEditor.getProject();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void reload() {
+ // If a details part has been created, we need to "refresh" it too.
+ if (mDetailsPart != null) {
+ // The details part does not directly expose access to its internal
+ // page book. Instead it is possible to resize the page book to 0 and then
+ // back to its original value, which has the side effect of removing all
+ // existing cached pages.
+ int limit = mDetailsPart.getPageLimit();
+ mDetailsPart.setPageLimit(0);
+ mDetailsPart.setPageLimit(limit);
+ }
+ // Refresh the tree, preserving the selection if possible.
+ mTreeViewer.refresh();
+ }
+ };
+
+ // Setup the listeners
+ changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */);
+
+ // Listen on resource framework changes to refresh the tree
+ AdtPlugin.getDefault().addTargetListener(targetListener);
+
+ // Remove listeners when the tree widget gets disposed.
+ tree.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ if (mUiRootNode != null) {
+ UiElementNode node = mUiRootNode.getUiParent() != null ?
+ mUiRootNode.getUiParent() :
+ mUiRootNode;
+
+ if (node != null) {
+ node.removeUpdateListener(mUiRefreshListener);
+ }
+ mUiRootNode.removeUpdateListener(mUiEnableListener);
+ }
+
+ AdtPlugin.getDefault().removeTargetListener(targetListener);
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+ }
+ });
+
+ // Get a new clipboard reference. It is disposed when the tree is disposed.
+ mClipboard = new Clipboard(tree.getDisplay());
+
+ return tree;
+ }
+
+ /**
+ * Changes the UI root node and the descriptor filters of the tree.
+ * <p/>
+ * This removes the listeners attached to the old root node and reattaches them to the
+ * new one.
+ *
+ * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
+ * to be manipulated by this tree view. In general this is the manifest UI node or the
+ * application UI node. This cannot be null.
+ * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
+ * this tree view. Use null or an empty list to accept any kind of node.
+ * @param forceRefresh If tree, forces the tree to refresh
+ */
+ public void changeRootAndDescriptors(UiElementNode uiRootNode,
+ ElementDescriptor[] descriptorFilters, boolean forceRefresh) {
+ UiElementNode node;
+
+ // Remove previous listeners if any
+ if (mUiRootNode != null) {
+ node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
+ node.removeUpdateListener(mUiRefreshListener);
+ mUiRootNode.removeUpdateListener(mUiEnableListener);
+ }
+
+ mUiRootNode = uiRootNode;
+ mDescriptorFilters = descriptorFilters;
+
+ mTreeViewer.setContentProvider(
+ new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
+
+ // Listen on structural changes on the root node of the tree
+ // If the node has a parent, listen on the parent instead.
+ if (mUiRootNode != null) {
+ node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
+
+ if (node != null) {
+ node.addUpdateListener(mUiRefreshListener);
+ }
+
+ // Use the root node to listen to its presence.
+ mUiRootNode.addUpdateListener(mUiEnableListener);
+
+ // Initialize the enabled/disabled state
+ mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */);
+ }
+
+ if (forceRefresh) {
+ mTreeViewer.refresh();
+ }
+
+ createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit());
+ }
+
+ /**
+ * Creates the buttons next to the tree.
+ */
+ private void createButtons(FormToolkit toolkit, Composite grid) {
+
+ mUiTreeActions = new UiTreeActions();
+
+ Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1);
+ button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
+ mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH);
+ SectionHelper.addControlTooltip(mAddButton, "Adds a new element.");
+ mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL |
+ GridData.VERTICAL_ALIGN_BEGINNING));
+
+ mAddButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeAdd();
+ }
+ });
+
+ mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element.");
+ mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mRemoveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeRemove();
+ }
+ });
+
+ mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up.");
+ mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mUpButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeUp();
+ }
+ });
+
+ mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down.");
+ mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mDownButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeDown();
+ }
+ });
+
+ adjustTreeButtons(TreeSelection.EMPTY);
+ }
+
+ private void createTreeContextMenu(Tree tree) {
+ MenuManager menuManager = new MenuManager();
+ menuManager.setRemoveAllWhenShown(true);
+ menuManager.addMenuListener(new IMenuListener() {
+ /**
+ * The menu is about to be shown. The menu manager has already been
+ * requested to remove any existing menu item. This method gets the
+ * tree selection and if it is of the appropriate type it re-creates
+ * the necessary actions.
+ */
+ @Override
+ public void menuAboutToShow(IMenuManager manager) {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ doCreateMenuAction(manager, selected);
+ return;
+ }
+ doCreateMenuAction(manager, null /* ui_node */);
+ }
+ });
+ Menu contextMenu = menuManager.createContextMenu(tree);
+ tree.setMenu(contextMenu);
+ }
+
+ /**
+ * Adds the menu actions to the context menu when the given UI node is selected in
+ * the tree view.
+ *
+ * @param manager The context menu manager
+ * @param selected The UI nodes selected in the tree. Can be null, in which case the root
+ * is to be modified.
+ */
+ private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) {
+ if (selected != null) {
+ boolean hasXml = false;
+ for (UiElementNode uiNode : selected) {
+ if (uiNode.getXmlNode() != null) {
+ hasXml = true;
+ break;
+ }
+ }
+
+ if (hasXml) {
+ manager.add(new CopyCutAction(getEditor(), getClipboard(),
+ null, selected, true /* cut */));
+ manager.add(new CopyCutAction(getEditor(), getClipboard(),
+ null, selected, false /* cut */));
+
+ // Can't paste with more than one element selected (the selection is the target)
+ if (selected.size() <= 1) {
+ // Paste is not valid if it would add a second element on a terminal element
+ // which parent is a document -- an XML document can only have one child. This
+ // means paste is valid if the current UI node can have children or if the
+ // parent is not a document.
+ UiElementNode ui_root = selected.get(0).getUiRoot();
+ if (ui_root.getDescriptor().hasChildren() ||
+ !(ui_root.getUiParent() instanceof UiDocumentNode)) {
+ manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0)));
+ }
+ }
+ manager.add(new Separator());
+ }
+ }
+
+ // Append "add" and "remove" actions. They do the same thing as the add/remove
+ // buttons on the side.
+ IconFactory factory = IconFactory.getInstance();
+
+ // "Add" makes sense only if there's 0 or 1 item selected since the
+ // one selected item becomes the target.
+ if (selected == null || selected.size() <= 1) {
+ manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeAdd();
+ }
+ });
+ }
+
+ if (selected != null) {
+ if (selected != null) {
+ manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeRemove();
+ }
+ });
+ }
+ manager.add(new Separator());
+
+ manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeUp();
+ }
+ });
+ manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeDown();
+ }
+ });
+ }
+ }
+
+
+ /**
+ * This is called by the tree when a selection is made.
+ * It enables/disables the buttons associated with the tree depending on the current
+ * selection.
+ *
+ * @param selection The current tree selection (same as mTreeViewer.getSelection())
+ */
+ private void adjustTreeButtons(ISelection selection) {
+ mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
+ mUpButton.setEnabled(canDoTreeUp(selection));
+ mDownButton.setEnabled(canDoTreeDown(selection));
+ }
+
+ /**
+ * An adapter/wrapper to use the add/remove/up/down tree edit actions.
+ */
+ private class UiTreeActions extends UiActions {
+ @Override
+ protected UiElementNode getRootNode() {
+ return mUiRootNode;
+ }
+
+ @Override
+ protected void selectUiNode(UiElementNode uiNodeToSelect) {
+ // Select the new item
+ if (uiNodeToSelect != null) {
+ LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>();
+ for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode;
+ ui_node = ui_node.getUiParent()) {
+ segments.add(0, ui_node);
+ }
+ if (segments.size() > 0) {
+ mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray())));
+ } else {
+ mTreeViewer.setSelection(null);
+ }
+ }
+ }
+
+ @Override
+ public void commitPendingXmlChanges() {
+ commitManagedForm();
+ }
+ }
+
+ /**
+ * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's
+ * something else in there).
+ *
+ * @return A new list of {@link UiElementNode} with at least one item or null.
+ */
+ private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) {
+ ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
+
+ for (Iterator<Object> it = selection.iterator(); it.hasNext(); ) {
+ Object selectedObj = it.next();
+
+ if (selectedObj instanceof UiElementNode) {
+ selected.add((UiElementNode) selectedObj);
+ }
+ }
+
+ return selected.size() > 0 ? selected : null;
+ }
+
+ /**
+ * Called when the "Add..." button next to the tree view is selected.
+ *
+ * Displays a selection dialog that lets the user select which kind of node
+ * to create, depending on the current selection.
+ */
+ private void doTreeAdd() {
+ UiElementNode ui_node = mUiRootNode;
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ITreeSelection tree_selection = (ITreeSelection) selection;
+ Object first = tree_selection.getFirstElement();
+ if (first != null && first instanceof UiElementNode) {
+ ui_node = (UiElementNode) first;
+ }
+ }
+
+ mUiTreeActions.doAdd(
+ ui_node,
+ mDescriptorFilters,
+ mTreeViewer.getControl().getShell(),
+ (ILabelProvider) mTreeViewer.getLabelProvider());
+ }
+
+ /**
+ * Called when the "Remove" button is selected.
+ *
+ * If the tree has a selection, remove it.
+ * This simply deletes the XML node attached to the UI node: when the XML model fires the
+ * update event, the tree will get refreshed.
+ */
+ protected void doTreeRemove() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell());
+ }
+ }
+
+ /**
+ * Called when the "Up" button is selected.
+ * <p/>
+ * If the tree has a selection, move it up, either in the child list or as the last child
+ * of the previous parent.
+ */
+ protected void doTreeUp() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doUp(selected, mDescriptorFilters);
+ }
+ }
+
+ /**
+ * Checks whether the "up" action can be done on the current selection.
+ *
+ * @param selection The current tree selection.
+ * @return True if all the selected nodes can be moved up.
+ */
+ protected boolean canDoTreeUp(ISelection selection) {
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ return mUiTreeActions.canDoUp(selected, mDescriptorFilters);
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when the "Down" button is selected.
+ *
+ * If the tree has a selection, move it down, either in the same child list or as the
+ * first child of the next parent.
+ */
+ protected void doTreeDown() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doDown(selected, mDescriptorFilters);
+ }
+ }
+
+ /**
+ * Checks whether the "down" action can be done on the current selection.
+ *
+ * @param selection The current tree selection.
+ * @return True if all the selected nodes can be moved down.
+ */
+ protected boolean canDoTreeDown(ISelection selection) {
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ return mUiTreeActions.canDoDown(selected, mDescriptorFilters);
+ }
+
+ return false;
+ }
+
+ /**
+ * Commits the current managed form (the one associated with our master part).
+ * As a side effect, this will commit the current UiElementDetails page.
+ */
+ void commitManagedForm() {
+ if (mManagedForm != null) {
+ mManagedForm.commit(false /* onSave */);
+ }
+ }
+
+ /* Implements ICommitXml for CopyCutAction */
+ @Override
+ public void commitPendingXmlChanges() {
+ commitManagedForm();
+ }
+
+ @Override
+ protected void createToolBarActions(IManagedForm managedForm) {
+ // Pass. Not used, toolbar actions are defined by createSectionActions().
+ }
+
+ @Override
+ protected void registerPages(DetailsPart inDetailsPart) {
+ // Keep a reference on the details part (the super class doesn't provide a getter
+ // for it.)
+ mDetailsPart = inDetailsPart;
+
+ // The page selection mechanism does not use pages registered by association with
+ // a node class. Instead it uses a custom details page provider that provides a
+ // new UiElementDetail instance for each node instance. A limit of 5 pages is
+ // then set (the value is arbitrary but should be reasonable) for the internal
+ // page book.
+ inDetailsPart.setPageLimit(5);
+
+ final UiTreeBlock tree = this;
+
+ inDetailsPart.setPageProvider(new IDetailsPageProvider() {
+ @Override
+ public IDetailsPage getPage(Object key) {
+ if (key instanceof UiElementNode) {
+ return new UiElementDetail(tree);
+ }
+ return null;
+ }
+
+ @Override
+ public Object getPageKey(Object object) {
+ return object; // use node object as key
+ }
+ });
+ }
+
+ /**
+ * An alphabetic sort action for the tree viewer.
+ */
+ private class TreeSortAction extends Action {
+
+ private ViewerComparator mComparator;
+
+ public TreeSortAction() {
+ super("Sorts elements alphabetically.", AS_CHECK_BOX);
+ setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$
+
+ if (mTreeViewer != null) {
+ boolean is_sorted = mTreeViewer.getComparator() != null;
+ setChecked(is_sorted);
+ }
+ }
+
+ /**
+ * Called when the button is selected. Toggles the tree viewer comparator.
+ */
+ @Override
+ public void run() {
+ if (mTreeViewer == null) {
+ notifyResult(false /*success*/);
+ return;
+ }
+
+ ViewerComparator comp = mTreeViewer.getComparator();
+ if (comp != null) {
+ // Tree is currently sorted.
+ // Save currently comparator and remove it
+ mComparator = comp;
+ mTreeViewer.setComparator(null);
+ } else {
+ // Tree is not currently sorted.
+ // Reuse or add a new comparator.
+ if (mComparator == null) {
+ mComparator = new ViewerComparator();
+ }
+ mTreeViewer.setComparator(mComparator);
+ }
+
+ notifyResult(true /*success*/);
+ }
+ }
+
+ /**
+ * A filter on descriptor for the tree viewer.
+ * <p/>
+ * The tree viewer will contain many of these actions and only one can be enabled at a
+ * given time. When no action is selected, everything is displayed.
+ * <p/>
+ * Since "radio"-like actions do not allow for unselecting all of them, we manually
+ * handle the exclusive radio button-like property: when an action is selected, it manually
+ * removes all other actions as needed.
+ */
+ private class DescriptorFilterAction extends Action {
+
+ private final ElementDescriptor mDescriptor;
+ private ViewerFilter mFilter;
+
+ public DescriptorFilterAction(ElementDescriptor descriptor) {
+ super(String.format("Displays only %1$s elements.", descriptor.getUiName()),
+ AS_CHECK_BOX);
+
+ mDescriptor = descriptor;
+ setImageDescriptor(descriptor.getImageDescriptor());
+ }
+
+ /**
+ * Called when the button is selected.
+ * <p/>
+ * Find any existing {@link DescriptorFilter}s and remove them. Install ours.
+ */
+ @Override
+ public void run() {
+ super.run();
+
+ if (isChecked()) {
+ if (mFilter == null) {
+ // create filter when required
+ mFilter = new DescriptorFilter(this);
+ }
+
+ // we add our filter first, otherwise the UI might show the full list
+ mTreeViewer.addFilter(mFilter);
+
+ // Then remove the any other filters except ours. There should be at most
+ // one other filter, since that's how the actions are made to look like
+ // exclusive radio buttons.
+ for (ViewerFilter filter : mTreeViewer.getFilters()) {
+ if (filter instanceof DescriptorFilter && filter != mFilter) {
+ DescriptorFilterAction action = ((DescriptorFilter) filter).getAction();
+ action.setChecked(false);
+ mTreeViewer.removeFilter(filter);
+ }
+ }
+ } else if (mFilter != null){
+ mTreeViewer.removeFilter(mFilter);
+ }
+ }
+
+ /**
+ * Filters the tree viewer for the given descriptor.
+ * <p/>
+ * The filter is linked to the action so that an action can iterate through the list
+ * of filters and un-select the actions.
+ */
+ private class DescriptorFilter extends ViewerFilter {
+
+ private final DescriptorFilterAction mAction;
+
+ public DescriptorFilter(DescriptorFilterAction action) {
+ mAction = action;
+ }
+
+ public DescriptorFilterAction getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns true if an element should be displayed, that if the element or
+ * any of its parent matches the requested descriptor.
+ */
+ @Override
+ public boolean select(Viewer viewer, Object parentElement, Object element) {
+ while (element instanceof UiElementNode) {
+ UiElementNode uiNode = (UiElementNode)element;
+ if (uiNode.getDescriptor() == mDescriptor) {
+ return true;
+ }
+ element = uiNode.getUiParent();
+ }
+ return false;
+ }
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java
new file mode 100644
index 000000000..dd908ad7b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+/**
+ * This interface decoration indicates that a given UiAttributeNode can both
+ * set and get its current value.
+ */
+public interface IUiSettableAttributeNode {
+
+ /** Returns the current value of the node. */
+ public String getCurrentValue();
+
+ /** Sets the current value of the node. Cannot be null (use an empty string). */
+ public void setCurrentValue(String value);
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java
new file mode 100644
index 000000000..a4f1f74ea
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+
+/**
+ * Listen to update notifications in UI nodes.
+ */
+public interface IUiUpdateListener {
+
+ /** Update state of the UI node */
+ public enum UiUpdateState {
+ /** The node's attributes have been updated. They may or may not actually have changed. */
+ ATTR_UPDATED,
+ /** The node sub-structure (i.e. child nodes) has changed */
+ CHILDREN_CHANGED,
+ /** The XML counterpart for the UI node has just been created. */
+ CREATED,
+ /** The XML counterpart for the UI node has just been deleted.
+ * Note that mandatory UI nodes are never actually deleted. */
+ DELETED
+ }
+
+ /**
+ * Indicates that an UiElementNode has been updated.
+ * <p/>
+ * This happens when an {@link UiElementNode} is refreshed to match the
+ * XML model. The actual UI element node may or may not have changed.
+ *
+ * @param ui_node The {@link UiElementNode} being updated.
+ */
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java
new file mode 100644
index 000000000..4f795904d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML attribute in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ * <p/>
+ * The XML attribute has no default value. When unset, the text field is blank.
+ * When updating the XML, if the field is empty, the attribute will be removed
+ * from the XML element.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public abstract class UiAbstractTextAttributeNode extends UiAttributeNode
+ implements IUiSettableAttributeNode {
+
+ protected static final String DEFAULT_VALUE = ""; //$NON-NLS-1$
+
+ /** Prevent internal listener from firing when internally modifying the text */
+ private boolean mInternalTextModification;
+ /** Last value read from the XML model. Cannot be null. */
+ private String mCurrentValue = DEFAULT_VALUE;
+
+ public UiAbstractTextAttributeNode(AttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /** Returns the current value of the node. */
+ @Override
+ public final String getCurrentValue() {
+ return mCurrentValue;
+ }
+
+ /** Sets the current value of the node. Cannot be null (use an empty string). */
+ @Override
+ public final void setCurrentValue(String value) {
+ mCurrentValue = value;
+ }
+
+ /** Returns if the attribute node is valid, and its UI has been created. */
+ public abstract boolean isValid();
+
+ /** Returns the text value present in the UI. */
+ public abstract String getTextWidgetValue();
+
+ /** Sets the text value to be displayed in the UI. */
+ public abstract void setTextWidgetValue(String value);
+
+
+ /**
+ * Updates the current text field's value when the XML has changed.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. The value
+ * is only set if it has changed.
+ * <p/>
+ * This also resets the "dirty" flag.
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ mCurrentValue = DEFAULT_VALUE;
+ if (xml_attribute_node != null) {
+ mCurrentValue = xml_attribute_node.getNodeValue();
+ }
+
+ if (isValid() && !getTextWidgetValue().equals(mCurrentValue)) {
+ try {
+ mInternalTextModification = true;
+ setTextWidgetValue(mCurrentValue);
+ setDirty(false);
+ } finally {
+ mInternalTextModification = false;
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ */
+ @Override
+ public void commit() {
+ UiElementNode parent = getUiParent();
+ if (parent != null && isValid() && isDirty()) {
+ String value = getTextWidgetValue();
+ if (parent.commitAttributeToXml(this, value)) {
+ mCurrentValue = value;
+ setDirty(false);
+ }
+ }
+ }
+
+ protected final boolean isInInternalTextModification() {
+ return mInternalTextModification;
+ }
+
+ protected final void setInInternalTextModification(boolean internalTextModification) {
+ mInternalTextModification = internalTextModification;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
new file mode 100644
index 000000000..ffe637c5d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.common.xml.XmlAttributeSortOrder;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML attribute that can be modified by the XML editor's user interface.
+ * <p/>
+ * The characteristics of an {@link UiAttributeNode} are declared by a
+ * corresponding {@link AttributeDescriptor}.
+ * <p/>
+ * This is an abstract class. Derived classes must implement the creation of the UI
+ * and manage its synchronization with the XML.
+ */
+public abstract class UiAttributeNode implements Comparable<UiAttributeNode> {
+
+ private AttributeDescriptor mDescriptor;
+ private UiElementNode mUiParent;
+ private boolean mIsDirty;
+ private boolean mHasError;
+
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor}
+ * and the corresponding runtime {@link UiElementNode} parent. */
+ public UiAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ mDescriptor = attributeDescriptor;
+ mUiParent = uiParent;
+ }
+
+ /** Returns the {@link AttributeDescriptor} specific to this UI attribute node */
+ public final AttributeDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /** Returns the {@link UiElementNode} that owns this {@link UiAttributeNode} */
+ public final UiElementNode getUiParent() {
+ return mUiParent;
+ }
+
+ /** Returns the current value of the node. */
+ public abstract String getCurrentValue();
+
+ /**
+ * @return True if the attribute has been changed since it was last loaded
+ * from the XML model.
+ */
+ public final boolean isDirty() {
+ return mIsDirty;
+ }
+
+ /**
+ * Sets whether the attribute is dirty and also notifies the editor some part's dirty
+ * flag as changed.
+ * <p/>
+ * Subclasses should set the to true as a result of user interaction with the widgets in
+ * the section and then should set to false when the commit() method completed.
+ *
+ * @param isDirty the new value to set the dirty-flag to
+ */
+ public void setDirty(boolean isDirty) {
+ boolean wasDirty = mIsDirty;
+ mIsDirty = isDirty;
+ // TODO: for unknown attributes, getParent() != null && getParent().getEditor() != null
+ if (wasDirty != isDirty) {
+ AndroidXmlEditor editor = getUiParent().getEditor();
+ if (editor != null) {
+ editor.editorDirtyStateChanged();
+ }
+ }
+ }
+
+ /**
+ * Sets the error flag value.
+ * @param errorFlag the error flag
+ */
+ public final void setHasError(boolean errorFlag) {
+ mHasError = errorFlag;
+ }
+
+ /**
+ * Returns whether this node has errors.
+ */
+ public final boolean hasError() {
+ return mHasError;
+ }
+
+ /**
+ * Called once by the parent user interface to creates the necessary
+ * user interface to edit this attribute.
+ * <p/>
+ * This method can be called more than once in the life cycle of an UI node,
+ * typically when the UI is part of a master-detail tree, as pages are swapped.
+ *
+ * @param parent The composite where to create the user interface.
+ * @param managedForm The managed form owning this part.
+ */
+ public abstract void createUiControl(Composite parent, IManagedForm managedForm);
+
+ /**
+ * Used to get a list of all possible values for this UI attribute.
+ * <p/>
+ * This is used, among other things, by the XML Content Assists to complete values
+ * for an attribute.
+ * <p/>
+ * Implementations that do not have any known values should return null.
+ *
+ * @param prefix An optional prefix string, which is whatever the user has already started
+ * typing. Can be null or an empty string. The implementation can use this to filter choices
+ * and only return strings that match this prefix. A lazy or default implementation can
+ * simply ignore this and return everything.
+ * @return A list of possible completion values, and empty array or null.
+ */
+ public abstract String[] getPossibleValues(String prefix);
+
+ /**
+ * Called when the XML is being loaded or has changed to
+ * update the value held by this user interface attribute node.
+ * <p/>
+ * The XML Node <em>may</em> be null, which denotes that the attribute is not
+ * specified in the XML model. In general, this means the "default" value of the
+ * attribute should be used.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. It's up to the
+ * UI implementation to minimize refreshes.
+ *
+ * @param node the node to read the value from
+ */
+ public abstract void updateValue(Node node);
+
+ /**
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ * <p/>
+ * Important behaviors:
+ * <ul>
+ * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before.
+ * The implemented methods must assume it is safe to modify the XML model.
+ * <li>On success, the implementation *must* call setDirty(false).
+ * <li>On failure, the implementation can fail with an exception, which
+ * is trapped and logged by the caller, or do nothing, whichever is more
+ * appropriate.
+ * </ul>
+ */
+ public abstract void commit();
+
+ // ---- Implements Comparable ----
+
+ @Override
+ public int compareTo(UiAttributeNode o) {
+ return XmlAttributeSortOrder.compareAttributes(mDescriptor.getXmlLocalName(),
+ o.mDescriptor.getXmlLocalName());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java
new file mode 100644
index 000000000..1a85ea682
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an XML document node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * The structure of a given {@link UiDocumentNode} is declared by a corresponding
+ * {@link DocumentDescriptor}.
+ */
+public class UiDocumentNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiDocumentNode} described by a given {@link DocumentDescriptor}.
+ *
+ * @param documentDescriptor The {@link DocumentDescriptor} for the XML node. Cannot be null.
+ */
+ public UiDocumentNode(DocumentDescriptor documentDescriptor) {
+ super(documentDescriptor);
+ }
+
+ /**
+ * Computes a short string describing the UI node suitable for tree views.
+ * Uses the element's attribute "android:name" if present, or the "android:label" one
+ * followed by the element's name.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ @Override
+ public String getShortDescription() {
+ return "Document"; //$NON-NLS-1$
+ }
+
+ /**
+ * Computes a "breadcrumb trail" description for this node.
+ *
+ * @param include_root Whether to include the root (e.g. "Manifest") or not. Has no effect
+ * when called on the root node itself.
+ * @return The "breadcrumb trail" description for this node.
+ */
+ @Override
+ public String getBreadcrumbTrailDescription(boolean include_root) {
+ return "Document"; //$NON-NLS-1$
+ }
+
+ /**
+ * This method throws an exception when attempted to assign a parent, since XML documents
+ * cannot have a parent. It is OK to assign null.
+ */
+ @Override
+ protected void setUiParent(UiElementNode parent) {
+ if (parent != null) {
+ // DEBUG. Change to log warning.
+ throw new UnsupportedOperationException("Documents can't have UI parents"); //$NON-NLS-1$
+ }
+ super.setUiParent(null);
+ }
+
+ /**
+ * Populate this element node with all values from the given XML node.
+ *
+ * This fails if the given XML node has a different element name -- it won't change the
+ * type of this ui node.
+ *
+ * This method can be both used for populating values the first time and updating values
+ * after the XML model changed.
+ *
+ * @param xml_node The XML node to mirror
+ * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
+ */
+ @Override
+ public boolean loadFromXmlNode(Node xml_node) {
+ boolean structure_changed = (getXmlDocument() != xml_node);
+ setXmlDocument((Document) xml_node);
+ structure_changed |= super.loadFromXmlNode(xml_node);
+ if (structure_changed) {
+ invokeUiUpdateListeners(UiUpdateState.CHILDREN_CHANGED);
+ }
+ return structure_changed;
+ }
+
+ /**
+ * This method throws an exception if there is no underlying XML document.
+ * <p/>
+ * XML documents cannot be created per se -- they are a by-product of the StructuredEditor
+ * XML parser.
+ *
+ * @return The current value of getXmlDocument().
+ */
+ @Override
+ public Node createXmlNode() {
+ if (getXmlDocument() == null) {
+ // By design, a document node cannot be created, it is owned by the XML parser.
+ // By "design" this should never happen since the XML parser always creates an XML
+ // document container, even for an empty file.
+ throw new UnsupportedOperationException("Documents cannot be created"); //$NON-NLS-1$
+ }
+ return getXmlDocument();
+ }
+
+ /**
+ * This method throws an exception and does not even try to delete the XML document.
+ * <p/>
+ * XML documents cannot be deleted per se -- they are a by-product of the StructuredEditor
+ * XML parser.
+ *
+ * @return The removed node or null if it didn't exist in the first place.
+ */
+ @Override
+ public Node deleteXmlNode() {
+ // DEBUG. Change to log warning.
+ throw new UnsupportedOperationException("Documents cannot be deleted"); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns all elements in this document.
+ *
+ * @param document the document
+ * @return all elements in the document
+ */
+ public static List<UiElementNode> getAllElements(UiDocumentNode document) {
+ List<UiElementNode> elements = new ArrayList<UiElementNode>(64);
+ for (UiElementNode child : document.getUiChildren()) {
+ addElements(child, elements);
+ }
+ return elements;
+ }
+
+ private static void addElements(UiElementNode node, List<UiElementNode> elements) {
+ elements.add(node);
+
+ for (UiElementNode child : node.getUiChildren()) {
+ addElements(child, elements);
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
new file mode 100644
index 000000000..ed447c634
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
@@ -0,0 +1,2160 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
+import static com.android.SdkConstants.ANDROID_SUPPORT_PKG_PREFIX;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+
+import com.android.SdkConstants;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.xml.XmlAttributeSortOrder;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.utils.SdkUtils;
+import com.android.utils.XmlUtils;
+
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+import org.eclipse.ui.views.properties.IPropertySource;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.document.ElementImpl;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Represents an XML node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * Each tree viewer used in the application page's parts needs to keep a model representing
+ * each underlying node in the tree. This interface represents the base type for such a node.
+ * <p/>
+ * Each node acts as an intermediary model between the actual XML model (the real data support)
+ * and the tree viewers or the corresponding page parts.
+ * <p/>
+ * Element nodes don't contain data per se. Their data is contained in their attributes
+ * as well as their children's attributes, see {@link UiAttributeNode}.
+ * <p/>
+ * The structure of a given {@link UiElementNode} is declared by a corresponding
+ * {@link ElementDescriptor}.
+ * <p/>
+ * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
+ * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
+ */
+@SuppressWarnings("restriction") // XML model
+public class UiElementNode implements IPropertySource {
+
+ /** List of prefixes removed from android:id strings when creating short descriptions. */
+ private static String[] ID_PREFIXES = {
+ "@android:id/", //$NON-NLS-1$
+ NEW_ID_PREFIX, ID_PREFIX, "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$
+
+ /** The element descriptor for the node. Always present, never null. */
+ private ElementDescriptor mDescriptor;
+ /** The parent element node in the UI model. It is null for a root element or until
+ * the node is attached to its parent. */
+ private UiElementNode mUiParent;
+ /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the
+ * root node. All children have the value set to null and query their parent. */
+ private AndroidXmlEditor mEditor;
+ /** The XML {@link Document} model that is being mirror by the UI model. This is defined
+ * only for the root node. All children have the value set to null and query their parent. */
+ private Document mXmlDocument;
+ /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
+ * have no corresponding XML node or for new UI nodes before their XML node is set. */
+ private Node mXmlNode;
+ /** The list of all UI children nodes. Can be empty but never null. There's one UI children
+ * node per existing XML children node. */
+ private ArrayList<UiElementNode> mUiChildren;
+ /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
+ * The list is always defined and never null. Unlike the UiElementNode children list, this
+ * is always defined, even for attributes that do not exist in the XML model - that's because
+ * "missing" attributes in the XML model simply mean a default value is used. Also note that
+ * the underlying collection is a map, so order is not respected. To get the desired attribute
+ * order, iterate through the {@link ElementDescriptor}'s attribute list. */
+ private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
+ private HashSet<UiAttributeNode> mUnknownUiAttributes;
+ /** A read-only view of the UI children node collection. */
+ private List<UiElementNode> mReadOnlyUiChildren;
+ /** A read-only view of the UI attributes collection. */
+ private Collection<UiAttributeNode> mCachedAllUiAttributes;
+ /** A map of hidden attribute descriptors. Key is the XML name. */
+ private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
+ /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
+ * listeners attached, so the list is only created on demand and can be null. */
+ private List<IUiUpdateListener> mUiUpdateListeners;
+ /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names.
+ * The default is to have one that creates new {@link ElementDescriptor}. */
+ private IUnknownDescriptorProvider mUnknownDescProvider;
+ /** Error Flag */
+ private boolean mHasError;
+
+ /**
+ * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
+ *
+ * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
+ */
+ public UiElementNode(ElementDescriptor elementDescriptor) {
+ mDescriptor = elementDescriptor;
+ clearContent();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s [desc: %s, parent: %s, children: %d]", //$NON-NLS-1$
+ this.getClass().getSimpleName(),
+ mDescriptor,
+ mUiParent != null ? mUiParent.toString() : "none", //$NON-NLS-1$
+ mUiChildren != null ? mUiChildren.size() : 0
+ );
+ }
+
+ /**
+ * Clears the {@link UiElementNode} by resetting the children list and
+ * the {@link UiAttributeNode}s list.
+ * Also resets the attached XML node, document, editor if any.
+ * <p/>
+ * The parent {@link UiElementNode} node is not reset so that it's position
+ * in the hierarchy be left intact, if any.
+ */
+ /* package */ void clearContent() {
+ mXmlNode = null;
+ mXmlDocument = null;
+ mEditor = null;
+ clearAttributes();
+ mReadOnlyUiChildren = null;
+ if (mUiChildren == null) {
+ mUiChildren = new ArrayList<UiElementNode>();
+ } else {
+ // We can't remove mandatory nodes, we just clear them.
+ for (int i = mUiChildren.size() - 1; i >= 0; --i) {
+ removeUiChildAtIndex(i);
+ }
+ }
+ }
+
+ /**
+ * Clears the internal list of attributes, the read-only cached version of it
+ * and the read-only cached hidden attribute list.
+ */
+ private void clearAttributes() {
+ mUiAttributes = null;
+ mCachedAllUiAttributes = null;
+ mCachedHiddenAttributes = null;
+ mUnknownUiAttributes = new HashSet<UiAttributeNode>();
+ }
+
+ /**
+ * Gets or creates the internal UiAttributes list.
+ * <p/>
+ * When the descriptor derives from ViewElementDescriptor, this list depends on the
+ * current UiParent node.
+ *
+ * @return A new set of {@link UiAttributeNode} that matches the expected
+ * attributes for this node.
+ */
+ private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
+ if (mUiAttributes == null) {
+ AttributeDescriptor[] attrList = getAttributeDescriptors();
+ mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attrList.length);
+ for (AttributeDescriptor desc : attrList) {
+ UiAttributeNode uiNode = desc.createUiNode(this);
+ if (uiNode != null) { // Some AttributeDescriptors do not have UI associated
+ mUiAttributes.put(desc, uiNode);
+ }
+ }
+ }
+ return mUiAttributes;
+ }
+
+ /**
+ * Computes a short string describing the UI node suitable for tree views.
+ * Uses the element's attribute "android:name" if present, or the "android:label" one
+ * followed by the element's name if not repeated.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ public String getShortDescription() {
+ String name = mDescriptor.getUiName();
+ String attr = getDescAttribute();
+ if (attr != null) {
+ // If the ui name is repeated in the attribute value, don't use it.
+ // Typical case is to avoid ".pkg.MyActivity (Activity)".
+ if (attr.contains(name)) {
+ return attr;
+ } else {
+ return String.format("%1$s (%2$s)", attr, name);
+ }
+ }
+
+ return name;
+ }
+
+ /** Returns the key attribute that can be used to describe this node, or null */
+ private String getDescAttribute() {
+ if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
+ // Application and Manifest nodes have a special treatment: they are unique nodes
+ // so we don't bother trying to differentiate their strings and we fall back to
+ // just using the UI name below.
+ Element elem = (Element) mXmlNode;
+
+ String attr = _Element_getAttributeNS(elem,
+ SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_NAME_ATTR);
+ if (attr == null || attr.length() == 0) {
+ attr = _Element_getAttributeNS(elem,
+ SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
+ } else if (mXmlNode.getNodeName().equals(SdkConstants.VIEW_FRAGMENT)) {
+ attr = attr.substring(attr.lastIndexOf('.') + 1);
+ }
+ if (attr == null || attr.length() == 0) {
+ attr = _Element_getAttributeNS(elem,
+ SdkConstants.NS_RESOURCES,
+ OtherXmlDescriptors.PREF_KEY_ATTR);
+ }
+ if (attr == null || attr.length() == 0) {
+ attr = _Element_getAttributeNS(elem,
+ null, // no namespace
+ SdkConstants.ATTR_NAME);
+ }
+ if (attr == null || attr.length() == 0) {
+ attr = _Element_getAttributeNS(elem,
+ SdkConstants.NS_RESOURCES,
+ SdkConstants.ATTR_ID);
+
+ if (attr != null && attr.length() > 0) {
+ for (String prefix : ID_PREFIXES) {
+ if (attr.startsWith(prefix)) {
+ attr = attr.substring(prefix.length());
+ break;
+ }
+ }
+ }
+ }
+ if (attr != null && attr.length() > 0) {
+ return attr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes a styled string describing the UI node suitable for tree views.
+ * Similar to {@link #getShortDescription()} but styles the Strings.
+ *
+ * @return A styled string describing the UI node suitable for tree views.
+ */
+ public StyledString getStyledDescription() {
+ String uiName = mDescriptor.getUiName();
+
+ // Special case: for <view>, show the class attribute value instead.
+ // This is done here rather than in the descriptor since this depends on
+ // node instance data.
+ if (SdkConstants.VIEW_TAG.equals(uiName) && mXmlNode instanceof Element) {
+ Element element = (Element) mXmlNode;
+ String cls = element.getAttribute(ATTR_CLASS);
+ if (cls != null) {
+ uiName = cls.substring(cls.lastIndexOf('.') + 1);
+ }
+ }
+
+ StyledString styledString = new StyledString();
+ String attr = getDescAttribute();
+ if (attr != null) {
+ // Don't append the two when it's a repeat, e.g. Button01 (Button),
+ // only when the ui name is not part of the attribute
+ if (attr.toLowerCase(Locale.US).indexOf(uiName.toLowerCase(Locale.US)) == -1) {
+ styledString.append(attr);
+ styledString.append(String.format(" (%1$s)", uiName),
+ StyledString.DECORATIONS_STYLER);
+ } else {
+ styledString.append(attr);
+ }
+ }
+
+ if (styledString.length() == 0) {
+ styledString.append(uiName);
+ }
+
+ return styledString;
+ }
+
+ /**
+ * Retrieves an attribute value by local name and namespace URI.
+ * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
+ * , applications must use the value <code>null</code> as the
+ * <code>namespaceURI</code> parameter for methods if they wish to have
+ * no namespace.
+ * <p/>
+ * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
+ * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
+ * This wrapper will return an empty string instead.
+ *
+ * @see Element#getAttributeNS(String, String)
+ * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
+ * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string.
+ */
+ private String _Element_getAttributeNS(Element element,
+ String namespaceURI,
+ String localName) {
+ try {
+ return element.getAttributeNS(namespaceURI, localName);
+ } catch (Exception ignore) {
+ return "";
+ }
+ }
+
+ /**
+ * Computes a "breadcrumb trail" description for this node.
+ * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
+ *
+ * @param includeRoot Whether to include the root (e.g. "Manifest") or not. Has no effect
+ * when called on the root node itself.
+ * @return The "breadcrumb trail" description for this node.
+ */
+ public String getBreadcrumbTrailDescription(boolean includeRoot) {
+ StringBuilder sb = new StringBuilder(getShortDescription());
+
+ for (UiElementNode uiNode = getUiParent();
+ uiNode != null;
+ uiNode = uiNode.getUiParent()) {
+ if (!includeRoot && uiNode.getUiParent() == null) {
+ break;
+ }
+ sb.insert(0, String.format("%1$s > ", uiNode.getShortDescription())); //$NON-NLS-1$
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Sets the XML {@link Document}.
+ * <p/>
+ * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
+ * UI root element node (this method takes care of that.)
+ * @param xmlDoc The new XML document to associate this node with.
+ */
+ public void setXmlDocument(Document xmlDoc) {
+ if (mUiParent == null) {
+ mXmlDocument = xmlDoc;
+ } else {
+ mUiParent.setXmlDocument(xmlDoc);
+ }
+ }
+
+ /**
+ * Returns the XML {@link Document}.
+ * <p/>
+ * The value is initially null until the UI node is attached to its UI parent -- the value
+ * of the document is then propagated.
+ *
+ * @return the XML {@link Document} or the parent's XML {@link Document} or null.
+ */
+ public Document getXmlDocument() {
+ if (mXmlDocument != null) {
+ return mXmlDocument;
+ } else if (mUiParent != null) {
+ return mUiParent.getXmlDocument();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the XML node associated with this UI node.
+ * <p/>
+ * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
+ * corresponding UI node will exist even if there is no corresponding XML node. Such structure
+ * is created and enforced by the parent of the tree, not the element themselves. However
+ * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
+ *
+ * @return The associated XML node. Can be null for mandatory nodes.
+ */
+ public Node getXmlNode() {
+ return mXmlNode;
+ }
+
+ /**
+ * Returns the {@link ElementDescriptor} for this node. This is never null.
+ * <p/>
+ * Do not use this to call getDescriptor().getAttributes(), instead call
+ * getAttributeDescriptors() which can be overridden by derived classes.
+ * @return The {@link ElementDescriptor} for this node. This is never null.
+ */
+ public ElementDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /**
+ * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
+ * <p/>
+ * Use this instead of getDescriptor().getAttributes() -- derived classes can override
+ * this to manipulate the attribute descriptor list depending on the current UI node.
+ * @return The {@link AttributeDescriptor} array for the descriptor of this node.
+ */
+ public AttributeDescriptor[] getAttributeDescriptors() {
+ return mDescriptor.getAttributes();
+ }
+
+ /**
+ * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
+ * This is a subset of the getAttributeDescriptors() list.
+ * <p/>
+ * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
+ * could override this to manipulate the attribute descriptor list depending on the current
+ * UI node. There's no need for it right now so keep it private.
+ */
+ private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
+ if (mCachedHiddenAttributes == null) {
+ mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
+ for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
+ if (attrDesc instanceof XmlnsAttributeDescriptor) {
+ mCachedHiddenAttributes.put(
+ ((XmlnsAttributeDescriptor) attrDesc).getXmlNsName(),
+ attrDesc);
+ }
+ }
+ }
+ return mCachedHiddenAttributes;
+ }
+
+ /**
+ * Sets the parent of this UiElementNode.
+ * <p/>
+ * The root node has no parent.
+ */
+ protected void setUiParent(UiElementNode parent) {
+ mUiParent = parent;
+ // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
+ clearAttributes();
+ }
+
+ /**
+ * @return The parent {@link UiElementNode} or null if this is the root node.
+ */
+ public UiElementNode getUiParent() {
+ return mUiParent;
+ }
+
+ /**
+ * Returns the root {@link UiElementNode}.
+ *
+ * @return The root {@link UiElementNode}.
+ */
+ public UiElementNode getUiRoot() {
+ UiElementNode root = this;
+ while (root.mUiParent != null) {
+ root = root.mUiParent;
+ }
+
+ return root;
+ }
+
+ /**
+ * Returns the index of this sibling (where the first child has index 0, the second child
+ * has index 1, and so on.)
+ *
+ * @return The sibling index of this node
+ */
+ public int getUiSiblingIndex() {
+ if (mUiParent != null) {
+ int index = 0;
+ for (UiElementNode node : mUiParent.getUiChildren()) {
+ if (node == this) {
+ break;
+ }
+ index++;
+ }
+ return index;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the previous UI sibling of this UI node. If the node does not have a previous
+ * sibling, returns null.
+ *
+ * @return The previous UI sibling of this UI node, or null if not applicable.
+ */
+ public UiElementNode getUiPreviousSibling() {
+ if (mUiParent != null) {
+ List<UiElementNode> childlist = mUiParent.getUiChildren();
+ if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
+ int index = childlist.indexOf(this);
+ return index > 0 ? childlist.get(index - 1) : null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the next UI sibling of this UI node.
+ * If the node does not have a next sibling, returns null.
+ *
+ * @return The next UI sibling of this UI node, or null.
+ */
+ public UiElementNode getUiNextSibling() {
+ if (mUiParent != null) {
+ List<UiElementNode> childlist = mUiParent.getUiChildren();
+ if (childlist != null) {
+ int size = childlist.size();
+ if (size > 1 && childlist.get(size - 1) != this) {
+ int index = childlist.indexOf(this);
+ return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
+ * <p/>
+ * The editor must always be set on the root node. This method takes care of that.
+ *
+ * @param editor The editor to associate this node with.
+ */
+ public void setEditor(AndroidXmlEditor editor) {
+ if (mUiParent == null) {
+ mEditor = editor;
+ } else {
+ mUiParent.setEditor(editor);
+ }
+ }
+
+ /**
+ * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}.
+ * <p/>
+ * The value is initially null until the node is attached to its parent -- the value
+ * of the root node is then propagated.
+ *
+ * @return The embedding {@link AndroidXmlEditor} or null.
+ */
+ public AndroidXmlEditor getEditor() {
+ return mUiParent == null ? mEditor : mUiParent.getEditor();
+ }
+
+ /**
+ * Returns the Android target data for the file being edited.
+ *
+ * @return The Android target data for the file being edited.
+ */
+ public AndroidTargetData getAndroidTarget() {
+ return getEditor().getTargetData();
+ }
+
+ /**
+ * @return A read-only version of the children collection.
+ */
+ public List<UiElementNode> getUiChildren() {
+ if (mReadOnlyUiChildren == null) {
+ mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
+ }
+ return mReadOnlyUiChildren;
+ }
+
+ /**
+ * Returns a collection containing all the known attributes as well as
+ * all the unknown ui attributes.
+ *
+ * @return A read-only version of the attributes collection.
+ */
+ public Collection<UiAttributeNode> getAllUiAttributes() {
+ if (mCachedAllUiAttributes == null) {
+
+ List<UiAttributeNode> allValues =
+ new ArrayList<UiAttributeNode>(getInternalUiAttributes().values());
+ allValues.addAll(mUnknownUiAttributes);
+
+ mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues);
+ }
+ return mCachedAllUiAttributes;
+ }
+
+ /**
+ * Returns all the unknown ui attributes, that is those we found defined in the
+ * actual XML but that we don't have descriptors for.
+ *
+ * @return A read-only version of the unknown attributes collection.
+ */
+ public Collection<UiAttributeNode> getUnknownUiAttributes() {
+ return Collections.unmodifiableCollection(mUnknownUiAttributes);
+ }
+
+ /**
+ * Sets the error flag value.
+ *
+ * @param errorFlag the error flag
+ */
+ public final void setHasError(boolean errorFlag) {
+ mHasError = errorFlag;
+ }
+
+ /**
+ * Returns whether this node, its attributes, or one of the children nodes (and attributes)
+ * has errors.
+ *
+ * @return True if this node, its attributes, or one of the children nodes (and attributes)
+ * has errors.
+ */
+ public final boolean hasError() {
+ if (mHasError) {
+ return true;
+ }
+
+ // get the error value from the attributes.
+ for (UiAttributeNode attribute : getAllUiAttributes()) {
+ if (attribute.hasError()) {
+ return true;
+ }
+ }
+
+ // and now from the children.
+ for (UiElementNode child : mUiChildren) {
+ if (child.hasError()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped
+ * XML names.
+ * <p/>
+ * The default is to have one that creates new {@link ElementDescriptor}.
+ * <p/>
+ * There is only one such provider in any UI model tree, attached to the root node.
+ *
+ * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null.
+ */
+ public IUnknownDescriptorProvider getUnknownDescriptorProvider() {
+ if (mUiParent != null) {
+ return mUiParent.getUnknownDescriptorProvider();
+ }
+ if (mUnknownDescProvider == null) {
+ // Create the default one on demand.
+ mUnknownDescProvider = new IUnknownDescriptorProvider() {
+
+ private final HashMap<String, ElementDescriptor> mMap =
+ new HashMap<String, ElementDescriptor>();
+
+ /**
+ * The default is to create a new ElementDescriptor wrapping
+ * the unknown XML local name and reuse previously created descriptors.
+ */
+ @Override
+ public ElementDescriptor getDescriptor(String xmlLocalName) {
+
+ ElementDescriptor desc = mMap.get(xmlLocalName);
+
+ if (desc == null) {
+ desc = new ElementDescriptor(xmlLocalName);
+ mMap.put(xmlLocalName, desc);
+ }
+
+ return desc;
+ }
+ };
+ }
+ return mUnknownDescProvider;
+ }
+
+ /**
+ * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped
+ * XML names.
+ * <p/>
+ * The default is to have one that creates new {@link ElementDescriptor}.
+ * <p/>
+ * There is only one such provider in any UI model tree, attached to the root node.
+ *
+ * @param unknownDescProvider The new provider to use. Must not be null.
+ */
+ public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) {
+ if (mUiParent == null) {
+ mUnknownDescProvider = unknownDescProvider;
+ } else {
+ mUiParent.setUnknownDescriptorProvider(unknownDescProvider);
+ }
+ }
+
+ /**
+ * Adds a new {@link IUiUpdateListener} to the internal update listener list.
+ *
+ * @param listener The listener to add.
+ */
+ public void addUpdateListener(IUiUpdateListener listener) {
+ if (mUiUpdateListeners == null) {
+ mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
+ }
+ if (!mUiUpdateListeners.contains(listener)) {
+ mUiUpdateListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
+ * Does nothing if the list is empty or the listener is not registered.
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeUpdateListener(IUiUpdateListener listener) {
+ if (mUiUpdateListeners != null) {
+ mUiUpdateListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Finds a child node relative to this node using a path-like expression.
+ * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
+ * returns the latter. If there are multiple nodes with the same name at the same
+ * level, always uses the first one found.
+ *
+ * @param path The path like expression to select a child node.
+ * @return The ui node found or null.
+ */
+ public UiElementNode findUiChildNode(String path) {
+ String[] items = path.split("/"); //$NON-NLS-1$
+ UiElementNode uiNode = this;
+ for (String item : items) {
+ boolean nextSegment = false;
+ for (UiElementNode c : uiNode.mUiChildren) {
+ if (c.getDescriptor().getXmlName().equals(item)) {
+ uiNode = c;
+ nextSegment = true;
+ break;
+ }
+ }
+ if (!nextSegment) {
+ return null;
+ }
+ }
+ return uiNode;
+ }
+
+ /**
+ * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
+ * Looks recursively in all children UI nodes.
+ *
+ * @param xmlNode The XML node to look for.
+ * @return The {@link UiElementNode} that contains xmlNode or null if not found,
+ */
+ public UiElementNode findXmlNode(Node xmlNode) {
+ if (xmlNode == null) {
+ return null;
+ }
+ if (getXmlNode() == xmlNode) {
+ return this;
+ }
+
+ for (UiElementNode uiChild : mUiChildren) {
+ UiElementNode found = uiChild.findXmlNode(xmlNode);
+ if (found != null) {
+ return found;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link UiAttributeNode} matching this attribute descriptor or
+ * null if not found.
+ *
+ * @param attrDesc The {@link AttributeDescriptor} to match.
+ * @return the {@link UiAttributeNode} matching this attribute descriptor or null
+ * if not found.
+ */
+ public UiAttributeNode findUiAttribute(AttributeDescriptor attrDesc) {
+ return getInternalUiAttributes().get(attrDesc);
+ }
+
+ /**
+ * Populate this element node with all values from the given XML node.
+ *
+ * This fails if the given XML node has a different element name -- it won't change the
+ * type of this ui node.
+ *
+ * This method can be both used for populating values the first time and updating values
+ * after the XML model changed.
+ *
+ * @param xmlNode The XML node to mirror
+ * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
+ */
+ public boolean loadFromXmlNode(Node xmlNode) {
+ boolean structureChanged = (mXmlNode != xmlNode);
+ mXmlNode = xmlNode;
+ if (xmlNode != null) {
+ updateAttributeList(xmlNode);
+ structureChanged |= updateElementList(xmlNode);
+ invokeUiUpdateListeners(structureChanged ? UiUpdateState.CHILDREN_CHANGED
+ : UiUpdateState.ATTR_UPDATED);
+ }
+ return structureChanged;
+ }
+
+ /**
+ * Clears the UI node and reload it from the given XML node.
+ * <p/>
+ * This works by clearing all references to any previous XML or UI nodes and
+ * then reloads the XML document from scratch. The editor reference is kept.
+ * <p/>
+ * This is used in the special case where the ElementDescriptor structure has changed.
+ * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
+ * and reload everything. This is not subtle and should be used very rarely.
+ *
+ * @param xmlNode The XML node or document to reload. Can be null.
+ */
+ public void reloadFromXmlNode(Node xmlNode) {
+ // The editor needs to be preserved, it is not affected by an XML change.
+ AndroidXmlEditor editor = getEditor();
+ clearContent();
+ setEditor(editor);
+ if (xmlNode != null) {
+ setXmlDocument(xmlNode.getOwnerDocument());
+ }
+ // This will reload all the XML and recreate the UI structure from scratch.
+ loadFromXmlNode(xmlNode);
+ }
+
+ /**
+ * Called by attributes when they want to commit their value
+ * to an XML node.
+ * <p/>
+ * For mandatory nodes, this makes sure the underlying XML element node
+ * exists in the model. If not, it is created and assigned as the underlying
+ * XML node.
+ * </br>
+ * For non-mandatory nodes, simply return the underlying XML node, which
+ * must always exists.
+ *
+ * @return The XML node matching this {@link UiElementNode} or null.
+ */
+ public Node prepareCommit() {
+ if (getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
+ createXmlNode();
+ // The new XML node has been created.
+ // We don't need to refresh using loadFromXmlNode() since there are
+ // no attributes or elements that need to be loading into this node.
+ }
+ return getXmlNode();
+ }
+
+ /**
+ * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
+ * This is called by the UI when the embedding part needs to be committed.
+ */
+ public void commit() {
+ for (UiAttributeNode uiAttr : getAllUiAttributes()) {
+ uiAttr.commit();
+ }
+ }
+
+ /**
+ * Returns true if the part has been modified with respect to the data
+ * loaded from the model.
+ * @return True if the part has been modified with respect to the data
+ * loaded from the model.
+ */
+ public boolean isDirty() {
+ for (UiAttributeNode uiAttr : getAllUiAttributes()) {
+ if (uiAttr.isDirty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates the underlying XML element node for this UI node if it doesn't already
+ * exists.
+ *
+ * @return The new value of getXmlNode() (can be null if creation failed)
+ */
+ public Node createXmlNode() {
+ if (mXmlNode != null) {
+ return null;
+ }
+ Node parentXmlNode = null;
+ if (mUiParent != null) {
+ parentXmlNode = mUiParent.prepareCommit();
+ if (parentXmlNode == null) {
+ // The parent failed to create its own backing XML node. Abort.
+ // No need to throw an exception, the parent will most likely
+ // have done so itself.
+ return null;
+ }
+ }
+
+ String elementName = getDescriptor().getXmlName();
+ Document doc = getXmlDocument();
+
+ // We *must* have a root node. If not, we need to abort.
+ if (doc == null) {
+ throw new RuntimeException(
+ String.format("Missing XML document for %1$s XML node.", elementName));
+ }
+
+ // If we get here and parentXmlNode is null, the node is to be created
+ // as the root node of the document (which can't be null, cf. check above).
+ if (parentXmlNode == null) {
+ parentXmlNode = doc;
+ }
+
+ mXmlNode = doc.createElement(elementName);
+
+ // If this element does not have children, mark it as an empty tag
+ // such that the XML looks like <tag/> instead of <tag></tag>
+ if (!mDescriptor.hasChildren()) {
+ if (mXmlNode instanceof ElementImpl) {
+ ElementImpl element = (ElementImpl) mXmlNode;
+ element.setEmptyTag(true);
+ }
+ }
+
+ Node xmlNextSibling = null;
+
+ UiElementNode uiNextSibling = getUiNextSibling();
+ if (uiNextSibling != null) {
+ xmlNextSibling = uiNextSibling.getXmlNode();
+ }
+
+ Node previousTextNode = null;
+ if (xmlNextSibling != null) {
+ Node previousNode = xmlNextSibling.getPreviousSibling();
+ if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) {
+ previousTextNode = previousNode;
+ }
+ } else {
+ Node lastChild = parentXmlNode.getLastChild();
+ if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
+ previousTextNode = lastChild;
+ }
+ }
+
+ String insertAfter = null;
+
+ // Try to figure out the indentation node to insert. Even in auto-formatting
+ // we need to do this, because it turns out the XML editor's formatter does
+ // not do a very good job with completely botched up XML; it does a much better
+ // job if the new XML is already mostly well formatted. Thus, the main purpose
+ // of applying the real XML formatter after our own indentation attempts here is
+ // to make it apply its own tab-versus-spaces indentation properties, have it
+ // insert line breaks before attributes (if the user has configured that), etc.
+
+ // First figure out the indentation level of the newly inserted element;
+ // this is either the same as the previous sibling, or if there is no sibling,
+ // it's the indentation of the parent plus one indentation level.
+ boolean isFirstChild = getUiPreviousSibling() == null
+ || parentXmlNode.getFirstChild() == null;
+ AndroidXmlEditor editor = getEditor();
+ String indent;
+ String parentIndent = ""; //$NON-NLS-1$
+ if (isFirstChild) {
+ indent = parentIndent = editor.getIndent(parentXmlNode);
+ // We need to add one level of indentation. Are we using tabs?
+ // Can't get to formatting settings so let's just look at the
+ // parent indentation and see if we can guess
+ if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') {
+ indent = indent + '\t';
+ } else {
+ // Not using tabs, or we can't figure it out (because parent had no
+ // indentation). In that case, indent with 4 spaces, as seems to
+ // be the Android default.
+ indent = indent + " "; //$NON-NLS-1$
+ }
+ } else {
+ // Find out the indent of the previous sibling
+ indent = editor.getIndent(getUiPreviousSibling().getXmlNode());
+ }
+
+ // We want to insert the new element BEFORE the text node which precedes
+ // the next element, since that text node is the next element's indentation!
+ if (previousTextNode != null) {
+ xmlNextSibling = previousTextNode;
+ } else {
+ // If there's no previous text node, we are probably inside an
+ // empty element (<LinearLayout>|</LinearLayout>) and in that case we need
+ // to not only insert a newline and indentation before the new element, but
+ // after it as well.
+ insertAfter = parentIndent;
+ }
+
+ // Insert indent text node before the new element
+ IStructuredDocument document = editor.getStructuredDocument();
+ String newLine;
+ if (document != null) {
+ newLine = TextUtilities.getDefaultLineDelimiter(document);
+ } else {
+ newLine = SdkUtils.getLineSeparator();
+ }
+ Text indentNode = doc.createTextNode(newLine + indent);
+ parentXmlNode.insertBefore(indentNode, xmlNextSibling);
+
+ // Insert the element itself
+ parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
+
+ // Insert a separator after the tag. We only do this when we've inserted
+ // a tag into an area where there was no whitespace before
+ // (e.g. a new child of <LinearLayout></LinearLayout>).
+ if (insertAfter != null) {
+ Text sep = doc.createTextNode(newLine + insertAfter);
+ parentXmlNode.insertBefore(sep, xmlNextSibling);
+ }
+
+ // Set all initial attributes in the XML node if they are not empty.
+ // Iterate on the descriptor list to get the desired order and then use the
+ // internal values, if any.
+ List<UiAttributeNode> addAttributes = new ArrayList<UiAttributeNode>();
+
+ for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
+ if (attrDesc instanceof XmlnsAttributeDescriptor) {
+ XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attrDesc;
+ Attr attr = doc.createAttributeNS(SdkConstants.XMLNS_URI,
+ desc.getXmlNsName());
+ attr.setValue(desc.getValue());
+ attr.setPrefix(desc.getXmlNsPrefix());
+ mXmlNode.getAttributes().setNamedItemNS(attr);
+ } else {
+ UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc);
+
+ // Don't apply the attribute immediately, instead record this attribute
+ // such that we can gather all attributes and sort them first.
+ // This is necessary because the XML model will *append* all attributes
+ // so we want to add them in a particular order.
+ // (Note that we only have to worry about UiAttributeNodes with non null
+ // values, since this is a new node and we therefore don't need to attempt
+ // to remove existing attributes)
+ String value = uiAttr.getCurrentValue();
+ if (value != null && value.length() > 0) {
+ addAttributes.add(uiAttr);
+ }
+ }
+ }
+
+ // Sort and apply the attributes in order, because the Eclipse XML model will always
+ // append the XML attributes, so by inserting them in our desired order they will
+ // appear that way in the XML
+ Collections.sort(addAttributes);
+
+ for (UiAttributeNode node : addAttributes) {
+ commitAttributeToXml(node, node.getCurrentValue());
+ node.setDirty(false);
+ }
+
+ getEditor().scheduleNodeReformat(this, false);
+
+ // Notify per-node listeners
+ invokeUiUpdateListeners(UiUpdateState.CREATED);
+ // Notify global listeners
+ fireNodeCreated(this, getUiSiblingIndex());
+
+ return mXmlNode;
+ }
+
+ /**
+ * Removes the XML node corresponding to this UI node if it exists
+ * and also removes all mirrored information in this UI node (i.e. children, attributes)
+ *
+ * @return The removed node or null if it didn't exist in the first place.
+ */
+ public Node deleteXmlNode() {
+ if (mXmlNode == null) {
+ return null;
+ }
+
+ int previousIndex = getUiSiblingIndex();
+
+ // First clear the internals of the node and *then* actually deletes the XML
+ // node (because doing so will generate an update even and this node may be
+ // revisited via loadFromXmlNode).
+ Node oldXmlNode = mXmlNode;
+ clearContent();
+
+ Node xmlParent = oldXmlNode.getParentNode();
+ if (xmlParent == null) {
+ xmlParent = getXmlDocument();
+ }
+ Node previousSibling = oldXmlNode.getPreviousSibling();
+ oldXmlNode = xmlParent.removeChild(oldXmlNode);
+
+ // We need to remove the text node BEFORE the removed element, since THAT's the
+ // indentation node for the removed element.
+ if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
+ && previousSibling.getNodeValue().trim().length() == 0) {
+ xmlParent.removeChild(previousSibling);
+ }
+
+ invokeUiUpdateListeners(UiUpdateState.DELETED);
+ fireNodeDeleted(this, previousIndex);
+
+ return oldXmlNode;
+ }
+
+ /**
+ * Updates the element list for this UiElementNode.
+ * At the end, the list of children UiElementNode here will match the one from the
+ * provided XML {@link Node}:
+ * <ul>
+ * <li> Walk both the current ui children list and the xml children list at the same time.
+ * <li> If we have a new xml child but already reached the end of the ui child list, add the
+ * new xml node.
+ * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
+ * move it here. It means the XML child list has been reordered.
+ * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
+ * <li> At the end, we may have finished walking the xml child list but still have remaining
+ * ui children, simply delete them as they matching trailing xml nodes that have been
+ * removed unless they are mandatory ui nodes.
+ * </ul>
+ * Note that only the first case is used when populating the ui list the first time.
+ *
+ * @param xmlNode The XML node to mirror
+ * @return True when the XML structure has changed.
+ */
+ protected boolean updateElementList(Node xmlNode) {
+ boolean structureChanged = false;
+ boolean hasMandatoryLast = false;
+ int uiIndex = 0;
+ Node xmlChild = xmlNode.getFirstChild();
+ while (xmlChild != null) {
+ if (xmlChild.getNodeType() == Node.ELEMENT_NODE) {
+ String elementName = xmlChild.getNodeName();
+ UiElementNode uiNode = null;
+ CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
+ if (mUiChildren.size() <= uiIndex) {
+ // A new node is being added at the end of the list
+ ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
+ false /* recursive */);
+ if (desc == null && elementName.indexOf('.') != -1 &&
+ (!elementName.startsWith(ANDROID_PKG_PREFIX)
+ || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
+ AndroidXmlEditor editor = getEditor();
+ if (editor != null && editor.getProject() != null) {
+ desc = service.getDescriptor(editor.getProject(), elementName);
+ }
+ }
+ if (desc == null) {
+ // Unknown node. Create a temporary descriptor for it.
+ // We'll add unknown attributes to it later.
+ IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
+ desc = p.getDescriptor(elementName);
+ }
+ structureChanged = true;
+ uiNode = appendNewUiChild(desc);
+ uiIndex++;
+ } else {
+ // A new node is being inserted or moved.
+ // Note: mandatory nodes can be created without an XML node in which case
+ // getXmlNode() is null.
+ UiElementNode uiChild;
+ int n = mUiChildren.size();
+ for (int j = uiIndex; j < n; j++) {
+ uiChild = mUiChildren.get(j);
+ if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) {
+ if (j > uiIndex) {
+ // Found the same XML node at some later index, now move it here.
+ mUiChildren.remove(j);
+ mUiChildren.add(uiIndex, uiChild);
+ structureChanged = true;
+ }
+ uiNode = uiChild;
+ uiIndex++;
+ break;
+ }
+ }
+
+ if (uiNode == null) {
+ // Look for an unused mandatory node with no XML node attached
+ // referencing the same XML element name
+ for (int j = uiIndex; j < n; j++) {
+ uiChild = mUiChildren.get(j);
+ if (uiChild.getXmlNode() == null &&
+ uiChild.getDescriptor().getMandatory() !=
+ Mandatory.NOT_MANDATORY &&
+ uiChild.getDescriptor().getXmlName().equals(elementName)) {
+
+ if (j > uiIndex) {
+ // Found it, now move it here
+ mUiChildren.remove(j);
+ mUiChildren.add(uiIndex, uiChild);
+ }
+ // Assign the XML node to this empty mandatory element.
+ uiChild.mXmlNode = xmlChild;
+ structureChanged = true;
+ uiNode = uiChild;
+ uiIndex++;
+ }
+ }
+ }
+
+ if (uiNode == null) {
+ // Inserting new node
+ ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
+ false /* recursive */);
+ if (desc == null && elementName.indexOf('.') != -1 &&
+ (!elementName.startsWith(ANDROID_PKG_PREFIX)
+ || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
+ AndroidXmlEditor editor = getEditor();
+ if (editor != null && editor.getProject() != null) {
+ desc = service.getDescriptor(editor.getProject(), elementName);
+ }
+ }
+ if (desc == null) {
+ // Unknown node. Create a temporary descriptor for it.
+ // We'll add unknown attributes to it later.
+ IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
+ desc = p.getDescriptor(elementName);
+ } else {
+ structureChanged = true;
+ uiNode = insertNewUiChild(uiIndex, desc);
+ uiIndex++;
+ }
+ }
+ }
+ if (uiNode != null) {
+ // If we touched an UI Node, even an existing one, refresh its content.
+ // For new nodes, this will populate them recursively.
+ structureChanged |= uiNode.loadFromXmlNode(xmlChild);
+
+ // Remember if there are any mandatory-last nodes to reorder.
+ hasMandatoryLast |=
+ uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST;
+ }
+ }
+ xmlChild = xmlChild.getNextSibling();
+ }
+
+ // There might be extra UI nodes at the end if the XML node list got shorter.
+ for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) {
+ structureChanged |= removeUiChildAtIndex(index);
+ }
+
+ if (hasMandatoryLast) {
+ // At least one mandatory-last uiNode was moved. Let's see if we can
+ // move them back to the last position. That's possible if the only
+ // thing between these and the end are other mandatory empty uiNodes
+ // (mandatory uiNodes with no XML attached are pure "virtual" reserved
+ // slots and it's ok to reorganize them but other can't.)
+ int n = mUiChildren.size() - 1;
+ for (int index = n; index >= 0; index--) {
+ UiElementNode uiChild = mUiChildren.get(index);
+ Mandatory mand = uiChild.getDescriptor().getMandatory();
+ if (mand == Mandatory.MANDATORY_LAST && index < n) {
+ // Remove it from index and move it back at the end of the list.
+ mUiChildren.remove(index);
+ mUiChildren.add(uiChild);
+ } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) {
+ // We found at least one non-mandatory or a mandatory node with an actual
+ // XML attached, so there's nothing we can reorganize past this point.
+ break;
+ }
+ }
+ }
+
+ return structureChanged;
+ }
+
+ /**
+ * Internal helper to remove an UI child node given by its index in the
+ * internal child list.
+ *
+ * Also invokes the update listener on the node to be deleted *after* the node has
+ * been removed.
+ *
+ * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1
+ * @return True if the structure has changed
+ * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
+ * know that could never happen unless the computer is on fire or something.
+ */
+ private boolean removeUiChildAtIndex(int uiIndex) {
+ UiElementNode uiNode = mUiChildren.get(uiIndex);
+ ElementDescriptor desc = uiNode.getDescriptor();
+
+ try {
+ if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
+ // This is a mandatory node. Such a node must exist in the UiNode hierarchy
+ // even if there's no XML counterpart. However we only need to keep one.
+
+ // Check if the parent (e.g. this node) has another similar ui child node.
+ boolean keepNode = true;
+ for (UiElementNode child : mUiChildren) {
+ if (child != uiNode && child.getDescriptor() == desc) {
+ // We found another child with the same descriptor that is not
+ // the node we want to remove. This means we have one mandatory
+ // node so we can safely remove uiNode.
+ keepNode = false;
+ break;
+ }
+ }
+
+ if (keepNode) {
+ // We can't remove a mandatory node as we need to keep at least one
+ // mandatory node in the parent. Instead we just clear its content
+ // (including its XML Node reference).
+
+ // A mandatory node with no XML means it doesn't really exist, so it can't be
+ // deleted. So the structure will change only if the ui node is actually
+ // associated to an XML node.
+ boolean xmlExists = (uiNode.getXmlNode() != null);
+
+ uiNode.clearContent();
+ return xmlExists;
+ }
+ }
+
+ mUiChildren.remove(uiIndex);
+
+ return true;
+ } finally {
+ // Tell listeners that a node has been removed.
+ // The model has already been modified.
+ invokeUiUpdateListeners(UiUpdateState.DELETED);
+ }
+ }
+
+ /**
+ * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
+ * and appends it to the end of the element children list.
+ *
+ * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
+ * @return The new UI node that has been appended
+ */
+ public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
+ UiElementNode uiNode;
+ uiNode = descriptor.createUiNode();
+ mUiChildren.add(uiNode);
+ uiNode.setUiParent(this);
+ uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
+ return uiNode;
+ }
+
+ /**
+ * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
+ * and inserts it in the element children list at the specified position.
+ *
+ * @param index The position where to insert in the element children list.
+ * Shifts the element currently at that position (if any) and any
+ * subsequent elements to the right (adds one to their indices).
+ * Index must >= 0 and <= getUiChildren.size().
+ * Using size() means to append to the end of the list.
+ * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
+ * @return The new UI node.
+ */
+ public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
+ UiElementNode uiNode;
+ uiNode = descriptor.createUiNode();
+ mUiChildren.add(index, uiNode);
+ uiNode.setUiParent(this);
+ uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
+ return uiNode;
+ }
+
+ /**
+ * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}
+ * using the values from the XML element.
+ * <p/>
+ * For a given {@link UiElementNode}, the attribute list always exists in
+ * full and is totally independent of whether the XML model actually
+ * has the corresponding attributes.
+ * <p/>
+ * For each attribute declared in this {@link UiElementNode}, get
+ * the corresponding XML attribute. It may not exist, in which case the
+ * value will be null. We don't really know if a value has changed, so
+ * the updateValue() is called on the UI attribute in all cases.
+ *
+ * @param xmlNode The XML node to mirror
+ */
+ protected void updateAttributeList(Node xmlNode) {
+ NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
+ HashSet<Node> visited = new HashSet<Node>();
+
+ // For all known (i.e. expected) UI attributes, find an existing XML attribute of
+ // same (uri, local name) and update the internal Ui attribute value.
+ for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
+ AttributeDescriptor desc = uiAttr.getDescriptor();
+ if (!(desc instanceof SeparatorAttributeDescriptor)) {
+ Node xmlAttr = xmlAttrMap == null ? null :
+ xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
+ uiAttr.updateValue(xmlAttr);
+ visited.add(xmlAttr);
+ }
+ }
+
+ // Clone the current list of unknown attributes. We'll then remove from this list when
+ // we find attributes which are still unknown. What will be left are the old unknown
+ // attributes that have been deleted in the current XML attribute list.
+ @SuppressWarnings("unchecked")
+ HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();
+
+ // We need to ignore hidden attributes.
+ Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();
+
+ // Traverse the actual XML attribute list to find unknown attributes
+ if (xmlAttrMap != null) {
+ for (int i = 0; i < xmlAttrMap.getLength(); i++) {
+ Node xmlAttr = xmlAttrMap.item(i);
+ // Ignore attributes which have actual descriptors
+ if (visited.contains(xmlAttr)) {
+ continue;
+ }
+
+ String xmlFullName = xmlAttr.getNodeName();
+
+ // Ignore attributes which are hidden (based on the prefix:localName key)
+ if (hiddenAttrDesc.containsKey(xmlFullName)) {
+ continue;
+ }
+
+ String xmlAttrLocalName = xmlAttr.getLocalName();
+ String xmlNsUri = xmlAttr.getNamespaceURI();
+
+ UiAttributeNode uiAttr = null;
+ for (UiAttributeNode a : mUnknownUiAttributes) {
+ String aLocalName = a.getDescriptor().getXmlLocalName();
+ String aNsUri = a.getDescriptor().getNamespaceUri();
+ if (aLocalName.equals(xmlAttrLocalName) &&
+ (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
+ // This attribute is still present in the unknown list
+ uiAttr = a;
+ // It has not been deleted
+ deleted.remove(a);
+ break;
+ }
+ }
+ if (uiAttr == null) {
+ uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri);
+ }
+
+ uiAttr.updateValue(xmlAttr);
+ }
+
+ // Remove from the internal list unknown attributes that have been deleted from the xml
+ for (UiAttributeNode a : deleted) {
+ mUnknownUiAttributes.remove(a);
+ mCachedAllUiAttributes = null;
+ }
+ }
+ }
+
+ /**
+ * Create a new temporary text attribute descriptor for the unknown attribute
+ * and returns a new {@link UiAttributeNode} associated to this descriptor.
+ * <p/>
+ * The attribute is not marked as dirty, doing so is up to the caller.
+ */
+ private UiAttributeNode addUnknownAttribute(String xmlFullName,
+ String xmlAttrLocalName, String xmlNsUri) {
+ // Create a new unknown attribute of format string
+ TextAttributeDescriptor desc = new TextAttributeDescriptor(
+ xmlAttrLocalName, // xml name
+ xmlNsUri, // ui name
+ new AttributeInfo(xmlAttrLocalName, Format.STRING_SET)
+ );
+ UiAttributeNode uiAttr = desc.createUiNode(this);
+ mUnknownUiAttributes.add(uiAttr);
+ mCachedAllUiAttributes = null;
+ return uiAttr;
+ }
+
+ /**
+ * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node.
+ */
+ protected void invokeUiUpdateListeners(UiUpdateState state) {
+ if (mUiUpdateListeners != null) {
+ for (IUiUpdateListener listener : mUiUpdateListeners) {
+ try {
+ listener.uiElementNodeUpdated(this, state);
+ } catch (Exception e) {
+ // prevent a crashing listener from crashing the whole invocation chain
+ AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s", //$NON-NLS-1$
+ getBreadcrumbTrailDescription(true),
+ state.toString());
+ }
+ }
+ }
+ }
+
+ // --- for derived implementations only ---
+
+ @VisibleForTesting
+ public void setXmlNode(Node xmlNode) {
+ mXmlNode = xmlNode;
+ }
+
+ public void refreshUi() {
+ invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
+ }
+
+
+ // ------------- Helpers
+
+ /**
+ * Helper method to commit a single attribute value to XML.
+ * <p/>
+ * This method updates the XML regardless of the current XML value.
+ * Callers should check first if an update is needed.
+ * If the new value is empty, the XML attribute will be actually removed.
+ * <p/>
+ * Note that the caller MUST ensure that modifying the underlying XML model is
+ * safe and must take care of marking the model as dirty if necessary.
+ *
+ * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
+ *
+ * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
+ * @param newValue The new value to set.
+ * @return True if the XML attribute was modified or removed, false if nothing changed.
+ */
+ public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
+ // Get (or create) the underlying XML element node that contains the attributes.
+ Node element = prepareCommit();
+ if (element != null && uiAttr != null) {
+ String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
+ String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();
+
+ NamedNodeMap attrMap = element.getAttributes();
+ if (newValue == null || newValue.length() == 0) {
+ // Remove attribute if it's empty
+ if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
+ attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
+ return true;
+ }
+ } else {
+ // Add or replace an attribute
+ Document doc = element.getOwnerDocument();
+ if (doc != null) {
+ Attr attr;
+ if (attrNsUri != null && attrNsUri.length() > 0) {
+ attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName);
+ if (attr == null) {
+ attr = doc.createAttributeNS(attrNsUri, attrLocalName);
+ attr.setPrefix(XmlUtils.lookupNamespacePrefix(element, attrNsUri));
+ attrMap.setNamedItemNS(attr);
+ }
+ } else {
+ attr = (Attr) attrMap.getNamedItem(attrLocalName);
+ if (attr == null) {
+ attr = doc.createAttribute(attrLocalName);
+ attrMap.setNamedItem(attr);
+ }
+ }
+ attr.setValue(newValue);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Helper method to commit all dirty attributes values to XML.
+ * <p/>
+ * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has
+ * been called more than once and all the attributes marked as dirty must be committed to
+ * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
+ * attribute.
+ * <p/>
+ * Note that the caller MUST ensure that modifying the underlying XML model is
+ * safe and must take care of marking the model as dirty if necessary.
+ *
+ * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
+ *
+ * @return True if one or more values were actually modified or removed,
+ * false if nothing changed.
+ */
+ @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong
+ public boolean commitDirtyAttributesToXml() {
+ boolean result = false;
+ List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>();
+ for (UiAttributeNode uiAttr : getAllUiAttributes()) {
+ if (uiAttr.isDirty()) {
+ String value = uiAttr.getCurrentValue();
+ if (value != null && value.length() > 0) {
+ // Defer the new attributes: set these last and in order
+ dirtyAttributes.add(uiAttr);
+ } else {
+ result |= commitAttributeToXml(uiAttr, value);
+ uiAttr.setDirty(false);
+ }
+ }
+ }
+ if (dirtyAttributes.size() > 0) {
+ result = true;
+
+ Collections.sort(dirtyAttributes);
+
+ // The Eclipse XML model will *always* append new attributes.
+ // Therefore, if any of the dirty attributes are new, they will appear
+ // after any existing, clean attributes on the element. To fix this,
+ // we need to first remove any of these attributes, then insert them
+ // back in the right order.
+ Node element = prepareCommit();
+ if (element == null) {
+ return result;
+ }
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) {
+ // If auto formatting, don't bother with attribute sorting here since the
+ // order will be corrected as soon as the edit is committed anyway
+ for (UiAttributeNode uiAttribute : dirtyAttributes) {
+ commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
+ uiAttribute.setDirty(false);
+ }
+
+ return result;
+ }
+
+ AttributeDescriptor descriptor = dirtyAttributes.get(0).getDescriptor();
+ String firstName = descriptor.getXmlLocalName();
+ String firstNamePrefix = null;
+ String namespaceUri = descriptor.getNamespaceUri();
+ if (namespaceUri != null) {
+ firstNamePrefix = XmlUtils.lookupNamespacePrefix(element, namespaceUri);
+ }
+ NamedNodeMap attributes = ((Element) element).getAttributes();
+ List<Attr> move = new ArrayList<Attr>();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ if (XmlAttributeSortOrder.compareAttributes(
+ attribute.getPrefix(), attribute.getLocalName(),
+ firstNamePrefix, firstName) > 0) {
+ move.add(attribute);
+ }
+ }
+
+ for (Attr attribute : move) {
+ if (attribute.getNamespaceURI() != null) {
+ attributes.removeNamedItemNS(attribute.getNamespaceURI(),
+ attribute.getLocalName());
+ } else {
+ attributes.removeNamedItem(attribute.getName());
+ }
+ }
+
+ // Merge back the removed DOM attribute nodes and the new UI attribute nodes.
+ // In cases where the attribute DOM name and the UI attribute names equal,
+ // skip the DOM nodes and just apply the UI attributes.
+ int domAttributeIndex = 0;
+ int domAttributeIndexMax = move.size();
+ int uiAttributeIndex = 0;
+ int uiAttributeIndexMax = dirtyAttributes.size();
+
+ while (true) {
+ Attr domAttribute;
+ UiAttributeNode uiAttribute;
+
+ int compare;
+ if (uiAttributeIndex < uiAttributeIndexMax) {
+ if (domAttributeIndex < domAttributeIndexMax) {
+ domAttribute = move.get(domAttributeIndex);
+ uiAttribute = dirtyAttributes.get(uiAttributeIndex);
+
+ String domAttributeName = domAttribute.getLocalName();
+ String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName();
+ compare = XmlAttributeSortOrder.compareAttributes(domAttributeName,
+ uiAttributeName);
+ } else {
+ compare = 1;
+ uiAttribute = dirtyAttributes.get(uiAttributeIndex);
+ domAttribute = null;
+ }
+ } else if (domAttributeIndex < domAttributeIndexMax) {
+ compare = -1;
+ domAttribute = move.get(domAttributeIndex);
+ uiAttribute = null;
+ } else {
+ break;
+ }
+
+ if (compare < 0) {
+ if (domAttribute.getNamespaceURI() != null) {
+ attributes.setNamedItemNS(domAttribute);
+ } else {
+ attributes.setNamedItem(domAttribute);
+ }
+ domAttributeIndex++;
+ } else {
+ assert compare >= 0;
+ if (compare == 0) {
+ domAttributeIndex++;
+ }
+ commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
+ uiAttribute.setDirty(false);
+ uiAttributeIndex++;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Utility method to internally set the value of a text attribute for the current
+ * UiElementNode.
+ * <p/>
+ * This method is a helper. It silently ignores the errors such as the requested
+ * attribute not being present in the element or attribute not being settable.
+ * It accepts inherited attributes (such as layout).
+ * <p/>
+ * This does not commit to the XML model. It does mark the attribute node as dirty.
+ * This is up to the caller.
+ *
+ * @see #commitAttributeToXml(UiAttributeNode, String)
+ * @see #commitDirtyAttributesToXml()
+ *
+ * @param attrXmlName The XML <em>local</em> name of the attribute to modify
+ * @param attrNsUri The namespace URI of the attribute.
+ * Can be null if the attribute uses the global namespace.
+ * @param value The new value for the attribute. If set to null, the attribute is removed.
+ * @param override True if the value must be set even if one already exists.
+ * @return The {@link UiAttributeNode} that has been modified or null.
+ */
+ public UiAttributeNode setAttributeValue(
+ String attrXmlName,
+ String attrNsUri,
+ String value,
+ boolean override) {
+ if (value == null) {
+ value = ""; //$NON-NLS-1$ -- this removes an attribute
+ }
+
+ getEditor().scheduleNodeReformat(this, true);
+
+ // Try with all internal attributes
+ UiAttributeNode uiAttr = setInternalAttrValue(
+ getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
+ if (uiAttr != null) {
+ return uiAttr;
+ }
+
+ if (uiAttr == null) {
+ // Failed to find the attribute. For non-android attributes that is mostly expected,
+ // in which case we just create a new custom one. As a side effect, we'll find the
+ // attribute descriptor via getAllUiAttributes().
+ addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri);
+
+ // We've created the attribute, but not actually set the value on it, so let's do it.
+ // Try with the updated internal attributes.
+ // Implementation detail: we could just do a setCurrentValue + setDirty on the
+ // uiAttr returned by addUnknownAttribute(); however going through setInternalAttrValue
+ // means we won't duplicate the logic, at the expense of doing one more lookup.
+ uiAttr = setInternalAttrValue(
+ getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
+ }
+
+ return uiAttr;
+ }
+
+ private UiAttributeNode setInternalAttrValue(
+ Collection<UiAttributeNode> attributes,
+ String attrXmlName,
+ String attrNsUri,
+ String value,
+ boolean override) {
+
+ // For namespace less attributes (like the "layout" attribute of an <include> tag
+ // we may be passed "" as the namespace (during an attribute copy), and it
+ // should really be null instead.
+ if (attrNsUri != null && attrNsUri.length() == 0) {
+ attrNsUri = null;
+ }
+
+ for (UiAttributeNode uiAttr : attributes) {
+ AttributeDescriptor uiDesc = uiAttr.getDescriptor();
+
+ if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
+ // Both NS URI must be either null or equal.
+ if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) ||
+ (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) {
+
+ // Not all attributes are editable, ignore those which are not.
+ if (uiAttr instanceof IUiSettableAttributeNode) {
+ String current = uiAttr.getCurrentValue();
+ // Only update (and mark as dirty) if the attribute did not have any
+ // value or if the value was different.
+ if (override || current == null || !current.equals(value)) {
+ ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value);
+ // mark the attribute as dirty since their internal content
+ // as been modified, but not the underlying XML model
+ uiAttr.setDirty(true);
+ return uiAttr;
+ }
+ }
+
+ // We found the attribute but it's not settable. Since attributes are
+ // not duplicated, just abandon here.
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method to retrieve the internal value of an attribute.
+ * <p/>
+ * Note that this retrieves the *field* value if the attribute has some UI, and
+ * not the actual XML value. They may differ if the attribute is dirty.
+ *
+ * @param attrXmlName The XML name of the attribute to modify
+ * @return The current internal value for the attribute or null in case of error.
+ */
+ public String getAttributeValue(String attrXmlName) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
+ AttributeDescriptor uiDesc = entry.getKey();
+ if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
+ UiAttributeNode uiAttr = entry.getValue();
+ return uiAttr.getCurrentValue();
+ }
+ }
+ return null;
+ }
+
+ // ------ IPropertySource methods
+
+ @Override
+ public Object getEditableValue() {
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
+ *
+ * Returns the property descriptor for this node. Since the descriptors are not linked to the
+ * data, the AttributeDescriptor are used directly.
+ */
+ @Override
+ public IPropertyDescriptor[] getPropertyDescriptors() {
+ List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();
+
+ // get the standard descriptors
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+ Set<AttributeDescriptor> keys = attributeMap.keySet();
+
+
+ // we only want the descriptor that do implement the IPropertyDescriptor interface.
+ for (AttributeDescriptor key : keys) {
+ if (key instanceof IPropertyDescriptor) {
+ propDescs.add((IPropertyDescriptor)key);
+ }
+ }
+
+ // now get the descriptor from the unknown attributes
+ for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
+ if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
+ propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
+ }
+ }
+
+ // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
+ return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
+ *
+ * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
+ * which return the AttributeDescriptor itself.
+ */
+ @Override
+ public Object getPropertyValue(Object id) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+
+ if (attribute == null) {
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ return unknownAttr;
+ }
+ }
+ }
+
+ return attribute;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
+ *
+ * Returns whether the property is set. In our case this is if the string is non empty.
+ */
+ @Override
+ public boolean isPropertySet(Object id) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+
+ if (attribute != null) {
+ return attribute.getCurrentValue().length() > 0;
+ }
+
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ return unknownAttr.getCurrentValue().length() > 0;
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
+ *
+ * Reset the property to its default value. For now we simply empty it.
+ */
+ @Override
+ public void resetPropertyValue(Object id) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+ if (attribute != null) {
+ // TODO: reset the value of the attribute
+
+ return;
+ }
+
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ // TODO: reset the value of the attribute
+
+ return;
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
+ *
+ * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
+ * AttributeDescriptor itself. Value should be a String.
+ */
+ @Override
+ public void setPropertyValue(Object id, Object value) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+
+ if (attribute == null) {
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ attribute = unknownAttr;
+ break;
+ }
+ }
+ }
+
+ if (attribute != null) {
+
+ // get the current value and compare it to the new value
+ String oldValue = attribute.getCurrentValue();
+ final String newValue = (String)value;
+
+ if (oldValue.equals(newValue)) {
+ return;
+ }
+
+ final UiAttributeNode fAttribute = attribute;
+ AndroidXmlEditor editor = getEditor();
+ editor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ commitAttributeToXml(fAttribute, newValue);
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns true if this node is an ancestor (parent, grandparent, and so on)
+ * of the given node. Note that a node is not considered an ancestor of
+ * itself.
+ *
+ * @param node the node to test
+ * @return true if this node is an ancestor of the given node
+ */
+ public boolean isAncestorOf(UiElementNode node) {
+ node = node.getUiParent();
+ while (node != null) {
+ if (node == this) {
+ return true;
+ }
+ node = node.getUiParent();
+ }
+ return false;
+ }
+
+ /**
+ * Finds the nearest common parent of the two given nodes (which could be one of the
+ * two nodes as well)
+ *
+ * @param node1 the first node to test
+ * @param node2 the second node to test
+ * @return the nearest common parent of the two given nodes
+ */
+ public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) {
+ while (node2 != null) {
+ UiElementNode current = node1;
+ while (current != null && current != node2) {
+ current = current.getUiParent();
+ }
+ if (current == node2) {
+ return current;
+ }
+ node2 = node2.getUiParent();
+ }
+
+ return null;
+ }
+
+ // ---- Global node create/delete Listeners ----
+
+ /** List of listeners to be notified of newly created nodes, or null */
+ private static List<NodeCreationListener> sListeners;
+
+ /** Notify listeners that a new node has been created */
+ private void fireNodeCreated(UiElementNode newChild, int index) {
+ // Nothing to do if there aren't any listeners. We don't need to worry about
+ // the case where one thread is firing node changes while another is adding a listener
+ // (in that case it's still okay for this node firing not to be heard) so perform
+ // the check outside of synchronization.
+ if (sListeners == null) {
+ return;
+ }
+ synchronized (UiElementNode.class) {
+ if (sListeners != null) {
+ UiElementNode parent = newChild.getUiParent();
+ for (NodeCreationListener listener : sListeners) {
+ listener.nodeCreated(parent, newChild, index);
+ }
+ }
+ }
+ }
+
+ /** Notify listeners that a new node has been deleted */
+ private void fireNodeDeleted(UiElementNode oldChild, int index) {
+ if (sListeners == null) {
+ return;
+ }
+ synchronized (UiElementNode.class) {
+ if (sListeners != null) {
+ UiElementNode parent = oldChild.getUiParent();
+ for (NodeCreationListener listener : sListeners) {
+ listener.nodeDeleted(parent, oldChild, index);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a {@link NodeCreationListener} to be notified when new nodes are created
+ *
+ * @param listener the listener to be notified
+ */
+ public static void addNodeCreationListener(NodeCreationListener listener) {
+ synchronized (UiElementNode.class) {
+ if (sListeners == null) {
+ sListeners = new ArrayList<NodeCreationListener>(1);
+ }
+ sListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes a {@link NodeCreationListener} from the set of listeners such that it is
+ * no longer notified when nodes are created.
+ *
+ * @param listener the listener to be removed from the notification list
+ */
+ public static void removeNodeCreationListener(NodeCreationListener listener) {
+ synchronized (UiElementNode.class) {
+ sListeners.remove(listener);
+ if (sListeners.size() == 0) {
+ sListeners = null;
+ }
+ }
+ }
+
+ /** Interface implemented by listeners to be notified of newly created nodes */
+ public interface NodeCreationListener {
+ /**
+ * Called when a new child node is created and added to the given parent
+ *
+ * @param parent the parent of the created node
+ * @param child the newly node
+ * @param index the index among the siblings of the child <b>after</b>
+ * insertion
+ */
+ void nodeCreated(UiElementNode parent, UiElementNode child, int index);
+
+ /**
+ * Called when a child node is removed from the given parent
+ *
+ * @param parent the parent of the removed node
+ * @param child the removed node
+ * @param previousIndex the index among the siblings of the child
+ * <b>before</b> removal
+ */
+ void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java
new file mode 100644
index 000000000..13fcdb6b2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.FlagAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.dialogs.SelectionStatusDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents an XML attribute that is defined by a set of flag values,
+ * i.e. enum names separated by pipe (|) characters.
+ *
+ * Note: in Android resources, a "flag" is a list of fixed values where one or
+ * more values can be selected using an "or", e.g. "align='left|top'".
+ * By contrast, an "enum" is a list of fixed values of which only one can be
+ * selected at a given time, e.g. "gravity='right'".
+ * <p/>
+ * This class handles the "flag" case.
+ * The "enum" case is done using {@link UiListAttributeNode}.
+ */
+public class UiFlagAttributeNode extends UiTextAttributeNode {
+
+ public UiFlagAttributeNode(FlagAttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ final Button selectButton = toolkit.createButton(composite, "Select...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ selectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+
+ String currentText = getTextWidgetValue();
+
+ String result = showDialog(selectButton.getShell(), currentText);
+
+ if (result != null) {
+ setTextWidgetValue(result);
+ }
+ }
+ });
+ }
+
+ /**
+ * Get the flag names, either from the initial names set in the attribute
+ * or by querying the framework resource parser.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ String attr_name = getDescriptor().getXmlLocalName();
+ String element_name = getUiParent().getDescriptor().getXmlName();
+
+ String[] values = null;
+
+ if (getDescriptor() instanceof FlagAttributeDescriptor &&
+ ((FlagAttributeDescriptor) getDescriptor()).getNames() != null) {
+ // Get enum values from the descriptor
+ values = ((FlagAttributeDescriptor) getDescriptor()).getNames();
+ }
+
+ if (values == null) {
+ // or from the AndroidTargetData
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ values = data.getAttributeValues(element_name, attr_name);
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Shows a dialog letting the user choose a set of enum, and returns a string
+ * containing the result.
+ */
+ public String showDialog(Shell shell, String currentValue) {
+ FlagSelectionDialog dlg = new FlagSelectionDialog(
+ shell, currentValue.trim().split("\\s*\\|\\s*")); //$NON-NLS-1$
+ dlg.open();
+ Object[] result = dlg.getResult();
+ if (result != null) {
+ StringBuilder buf = new StringBuilder();
+ for (Object name : result) {
+ if (name instanceof String) {
+ if (buf.length() > 0) {
+ buf.append('|');
+ }
+ buf.append(name);
+ }
+ }
+
+ return buf.toString();
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Displays a list of flag names with checkboxes.
+ */
+ private class FlagSelectionDialog extends SelectionStatusDialog {
+
+ private Set<String> mCurrentSet;
+ private Table mTable;
+
+ public FlagSelectionDialog(Shell parentShell, String[] currentNames) {
+ super(parentShell);
+
+ mCurrentSet = new HashSet<String>();
+ for (String name : currentNames) {
+ if (name.length() > 0) {
+ mCurrentSet.add(name);
+ }
+ }
+
+ int shellStyle = getShellStyle();
+ setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
+ }
+
+ @Override
+ protected void computeResult() {
+ if (mTable != null) {
+ ArrayList<String> results = new ArrayList<String>();
+
+ for (TableItem item : mTable.getItems()) {
+ if (item.getChecked()) {
+ results.add((String)item.getData());
+ }
+ }
+
+ setResult(results);
+ }
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite composite= new Composite(parent, SWT.NONE);
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+ composite.setLayout(new GridLayout(1, true));
+ composite.setFont(parent.getFont());
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+ label.setText(String.format("Select the flag values for attribute %1$s:",
+ ((FlagAttributeDescriptor) getDescriptor()).getUiName()));
+
+ mTable = new Table(composite, SWT.CHECK | SWT.BORDER);
+ GridData data = new GridData();
+ // The 60,18 hints are the ones used by AbstractElementListSelectionDialog
+ data.widthHint = convertWidthInCharsToPixels(60);
+ data.heightHint = convertHeightInCharsToPixels(18);
+ data.grabExcessVerticalSpace = true;
+ data.grabExcessHorizontalSpace = true;
+ data.horizontalAlignment = GridData.FILL;
+ data.verticalAlignment = GridData.FILL;
+ mTable.setLayoutData(data);
+
+ mTable.setHeaderVisible(false);
+ final TableColumn column = new TableColumn(mTable, SWT.NONE);
+
+ // List all the expected flag names and check those which are currently used
+ String[] names = getPossibleValues(null);
+ if (names != null) {
+ for (String name : names) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(name);
+ item.setData(name);
+
+ boolean hasName = mCurrentSet.contains(name);
+ item.setChecked(hasName);
+ if (hasName) {
+ mCurrentSet.remove(name);
+ }
+ }
+ }
+
+ // If there are unknown flag names currently used, display them at the end if the
+ // table already checked.
+ if (!mCurrentSet.isEmpty()) {
+ FontDescriptor fontDesc = JFaceResources.getDialogFontDescriptor();
+ fontDesc = fontDesc.withStyle(SWT.ITALIC);
+ Font font = fontDesc.createFont(JFaceResources.getDialogFont().getDevice());
+
+ for (String name : mCurrentSet) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(String.format("%1$s (unknown flag)", name));
+ item.setData(name);
+ item.setChecked(true);
+ item.setFont(font);
+ }
+ }
+
+ // Add a listener that will resize the column to the full width of the table
+ // so that only one column appears in the table even if the dialog is resized.
+ ControlAdapter listener = new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = mTable.getClientArea();
+ column.setWidth(r.width);
+ }
+ };
+
+ mTable.addControlListener(listener);
+ listener.controlResized(null /* event not used */);
+
+ // Add a selection listener that will check/uncheck items when they are double-clicked
+ mTable.addSelectionListener(new SelectionAdapter() {
+ /** Default selection means double-click on "most" platforms */
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ if (e.item instanceof TableItem) {
+ TableItem i = (TableItem) e.item;
+ i.setChecked(!i.getChecked());
+ }
+ super.widgetDefaultSelected(e);
+ }
+ });
+
+ Dialog.applyDialogFont(composite);
+ setHelpAvailable(false);
+
+ return composite;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java
new file mode 100644
index 000000000..0fd317c1c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute which has possible built-in values, and can be modified by
+ * an editable Combo box.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiListAttributeNode extends UiAbstractTextAttributeNode {
+
+ protected Combo mCombo;
+
+ public UiListAttributeNode(ListAttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public final void createUiControl(final Composite parent, IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ int style = SWT.DROP_DOWN;
+ mCombo = new Combo(parent, style);
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE);
+ twd.maxWidth = 100;
+ mCombo.setLayoutData(twd);
+
+ fillCombo();
+
+ setTextWidgetValue(getCurrentValue());
+
+ mCombo.addModifyListener(new ModifyListener() {
+ /**
+ * Sent when the text is modified, whether by the user via manual
+ * input or programmatic input via setText().
+ */
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onComboChange();
+ }
+ });
+
+ mCombo.addSelectionListener(new SelectionAdapter() {
+ /** Sent when the text is changed from a list selection. */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onComboChange();
+ }
+ });
+
+ // Remove self-reference when the widget is disposed
+ mCombo.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ mCombo = null;
+ }
+ });
+ }
+
+ protected void fillCombo() {
+ String[] values = getPossibleValues(null);
+
+ if (values == null) {
+ AdtPlugin.log(IStatus.ERROR,
+ "FrameworkResourceManager did not provide values yet for %1$s",
+ getDescriptor().getXmlLocalName());
+ } else {
+ for (String value : values) {
+ mCombo.add(value);
+ }
+ }
+ }
+
+ /**
+ * Get the list values, either from the initial values set in the attribute
+ * or by querying the framework resource parser.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ AttributeDescriptor descriptor = getDescriptor();
+ UiElementNode uiParent = getUiParent();
+
+ String attr_name = descriptor.getXmlLocalName();
+ String element_name = uiParent.getDescriptor().getXmlName();
+
+ // FrameworkResourceManager expects a specific prefix for the attribute.
+ String nsPrefix = "";
+ if (SdkConstants.NS_RESOURCES.equals(descriptor.getNamespaceUri())) {
+ nsPrefix = SdkConstants.ANDROID_NS_NAME + ':';
+ } else if (SdkConstants.XMLNS_URI.equals(descriptor.getNamespaceUri())) {
+ nsPrefix = SdkConstants.XMLNS_PREFIX;
+ }
+ attr_name = nsPrefix + attr_name;
+
+ String[] values = null;
+
+ if (descriptor instanceof ListAttributeDescriptor &&
+ ((ListAttributeDescriptor) descriptor).getValues() != null) {
+ // Get enum values from the descriptor
+ values = ((ListAttributeDescriptor) descriptor).getValues();
+ }
+
+ if (values == null) {
+ // or from the AndroidTargetData
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ // get the great-grand-parent descriptor.
+
+ // the parent should always exist.
+ UiElementNode grandParentNode = uiParent.getUiParent();
+
+ String greatGrandParentNodeName = null;
+ if (grandParentNode != null) {
+ UiElementNode greatGrandParentNode = grandParentNode.getUiParent();
+ if (greatGrandParentNode != null) {
+ greatGrandParentNodeName =
+ greatGrandParentNode.getDescriptor().getXmlName();
+ }
+ }
+
+ values = data.getAttributeValues(element_name, attr_name, greatGrandParentNodeName);
+ }
+ }
+
+ return values;
+ }
+
+ @Override
+ public String getTextWidgetValue() {
+ if (mCombo != null) {
+ return mCombo.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ public final boolean isValid() {
+ return mCombo != null;
+ }
+
+ @Override
+ public void setTextWidgetValue(String value) {
+ if (mCombo != null) {
+ mCombo.setText(value);
+ }
+ }
+
+ /**
+ * Handles Combo change, either from text edit or from selection change.
+ * <p/>
+ * Simply mark the attribute as dirty if it really changed.
+ * The container SectionPart will collect these flag and manage them.
+ */
+ private void onComboChange() {
+ if (!isInInternalTextModification() &&
+ !isDirty() &&
+ mCombo != null &&
+ getCurrentValue() != null &&
+ !mCombo.getText().equals(getCurrentValue())) {
+ setDirty(true);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java
new file mode 100644
index 000000000..eb51d3f86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import static com.android.SdkConstants.ANDROID_PKG;
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.ATTR_STYLE;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents an XML attribute for a resource that can be modified using a simple text field or
+ * a dialog to choose an existing resource.
+ * <p/>
+ * It can be configured to represent any kind of resource, by providing the desired
+ * {@link ResourceType} in the constructor.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiResourceAttributeNode extends UiTextAttributeNode {
+ private ResourceType mType;
+
+ /**
+ * Creates a new {@linkplain UiResourceAttributeNode}
+ *
+ * @param type the associated resource type
+ * @param attributeDescriptor the attribute descriptor for this attribute
+ * @param uiParent the parent ui node, if any
+ */
+ public UiResourceAttributeNode(ResourceType type,
+ AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+
+ mType = type;
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ // TODO Add a validator using onAddModifyListener
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String result = showDialog(parent.getShell(), text.getText().trim());
+ if (result != null) {
+ text.setText(result);
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows a dialog letting the user choose a set of enum, and returns a
+ * string containing the result.
+ *
+ * @param shell the parent shell
+ * @param currentValue an initial value, if any
+ * @return the chosen string, or null
+ */
+ @Nullable
+ public String showDialog(@NonNull Shell shell, @Nullable String currentValue) {
+ // we need to get the project of the file being edited.
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ IProject project = editor.getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceRepository projectRepository =
+ ResourceManager.getInstance().getProjectResources(project);
+
+ if (mType != null) {
+ // get the Target Data to get the system resources
+ AndroidTargetData data = editor.getTargetData();
+ ResourceChooser dlg = ResourceChooser.create(project, mType, data, shell)
+ .setCurrentResource(currentValue);
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ } else {
+ ReferenceChooserDialog dlg = new ReferenceChooserDialog(
+ project,
+ projectRepository,
+ shell);
+
+ dlg.setCurrentResource(currentValue);
+
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets all the values one could use to auto-complete a "resource" value in an XML
+ * content assist.
+ * <p/>
+ * Typically the user is editing the value of an attribute in a resource XML, e.g.
+ * <pre> "&lt;Button android:test="@string/my_[caret]_string..." </pre>
+ * <p/>
+ *
+ * "prefix" is the value that the user has typed so far (or more exactly whatever is on the
+ * left side of the insertion point). In the example above it would be "@style/my_".
+ * <p/>
+ *
+ * To avoid a huge long list of values, the completion works on two levels:
+ * <ul>
+ * <li> If a resource type as been typed so far (e.g. "@style/"), then limit the values to
+ * the possible completions that match this type.
+ * <li> If no resource type as been typed so far, then return the various types that could be
+ * completed. So if the project has only strings and layouts resources, for example,
+ * the returned list will only include "@string/" and "@layout/".
+ * </ul>
+ *
+ * Finally if anywhere in the string we find the special token "android:", we use the
+ * current framework system resources rather than the project resources.
+ * This works for both "@android:style/foo" and "@style/android:foo" conventions even though
+ * the reconstructed name will always be of the former form.
+ *
+ * Note that "android:" here is a keyword specific to Android resources and should not be
+ * mixed with an XML namespace for an XML attribute name.
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ return computeResourceStringMatches(getUiParent().getEditor(), getDescriptor(), prefix);
+ }
+
+ /**
+ * Computes the set of resource string matches for a given resource prefix in a given editor
+ *
+ * @param editor the editor context
+ * @param descriptor the attribute descriptor, if any
+ * @param prefix the prefix, if any
+ * @return an array of resource string matches
+ */
+ @Nullable
+ public static String[] computeResourceStringMatches(
+ @NonNull AndroidXmlEditor editor,
+ @Nullable AttributeDescriptor descriptor,
+ @Nullable String prefix) {
+
+ if (prefix == null || !prefix.regionMatches(1, ANDROID_PKG, 0, ANDROID_PKG.length())) {
+ IProject project = editor.getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceManager resourceManager = ResourceManager.getInstance();
+ ResourceRepository repository = resourceManager.getProjectResources(project);
+
+ List<IProject> libraries = null;
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null) {
+ libraries = projectState.getFullLibraryProjects();
+ }
+
+ String[] projectMatches = computeResourceStringMatches(descriptor, prefix,
+ repository, false);
+
+ if (libraries == null || libraries.isEmpty()) {
+ return projectMatches;
+ }
+
+ // Also compute matches for each of the libraries, and combine them
+ Set<String> matches = new HashSet<String>(200);
+ for (String s : projectMatches) {
+ matches.add(s);
+ }
+
+ for (IProject library : libraries) {
+ repository = resourceManager.getProjectResources(library);
+ projectMatches = computeResourceStringMatches(descriptor, prefix,
+ repository, false);
+ for (String s : projectMatches) {
+ matches.add(s);
+ }
+ }
+
+ String[] sorted = matches.toArray(new String[matches.size()]);
+ Arrays.sort(sorted);
+ return sorted;
+ }
+ } else {
+ // If there's a prefix with "android:" in it, use the system resources
+ // Non-public framework resources are filtered out later.
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ ResourceRepository repository = data.getFrameworkResources();
+ return computeResourceStringMatches(descriptor, prefix, repository, true);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes the set of resource string matches for a given prefix and a
+ * given resource repository
+ *
+ * @param attributeDescriptor the attribute descriptor, if any
+ * @param prefix the prefix, if any
+ * @param repository the repository to seaerch in
+ * @param isSystem if true, the repository contains framework repository,
+ * otherwise it contains project repositories
+ * @return an array of resource string matches
+ */
+ @NonNull
+ public static String[] computeResourceStringMatches(
+ @Nullable AttributeDescriptor attributeDescriptor,
+ @Nullable String prefix,
+ @NonNull ResourceRepository repository,
+ boolean isSystem) {
+ // Get list of potential resource types, either specific to this project
+ // or the generic list.
+ Collection<ResourceType> resTypes = (repository != null) ?
+ repository.getAvailableResourceTypes() :
+ EnumSet.allOf(ResourceType.class);
+
+ // Get the type name from the prefix, if any. It's any word before the / if there's one
+ String typeName = null;
+ if (prefix != null) {
+ Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix); //$NON-NLS-1$
+ if (m.matches()) {
+ typeName = m.group(1);
+ }
+ }
+
+ // Now collect results
+ List<String> results = new ArrayList<String>();
+
+ if (typeName == null) {
+ // This prefix does not have a / in it, so the resource string is either empty
+ // or does not have the resource type in it. Simply offer the list of potential
+ // resource types.
+ if (prefix != null && prefix.startsWith(PREFIX_THEME_REF)) {
+ results.add(ANDROID_THEME_PREFIX + ResourceType.ATTR.getName() + '/');
+ if (resTypes.contains(ResourceType.ATTR)
+ || resTypes.contains(ResourceType.STYLE)) {
+ results.add(PREFIX_THEME_REF + ResourceType.ATTR.getName() + '/');
+ if (prefix != null && prefix.startsWith(ANDROID_THEME_PREFIX)) {
+ // including attr isn't required
+ for (ResourceItem item : repository.getResourceItemsOfType(
+ ResourceType.ATTR)) {
+ results.add(ANDROID_THEME_PREFIX + item.getName());
+ }
+ }
+ }
+ return results.toArray(new String[results.size()]);
+ }
+
+ for (ResourceType resType : resTypes) {
+ if (isSystem) {
+ results.add(ANDROID_PREFIX + resType.getName() + '/');
+ } else {
+ results.add('@' + resType.getName() + '/');
+ }
+ if (resType == ResourceType.ID) {
+ // Also offer the + version to create an id from scratch
+ results.add("@+" + resType.getName() + '/'); //$NON-NLS-1$
+ }
+ }
+
+ // Also add in @android: prefix to completion such that if user has typed
+ // "@an" we offer to complete it.
+ if (prefix == null ||
+ ANDROID_PKG.regionMatches(0, prefix, 1, prefix.length() - 1)) {
+ results.add(ANDROID_PREFIX);
+ }
+ } else if (repository != null) {
+ // We have a style name and a repository. Find all resources that match this
+ // type and recreate suggestions out of them.
+
+ String initial = prefix != null && prefix.startsWith(PREFIX_THEME_REF)
+ ? PREFIX_THEME_REF : PREFIX_RESOURCE_REF;
+ ResourceType resType = ResourceType.getEnum(typeName);
+ if (resType != null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(initial);
+ if (prefix != null && prefix.indexOf('+') >= 0) {
+ sb.append('+');
+ }
+
+ if (isSystem) {
+ sb.append(ANDROID_PKG).append(':');
+ }
+
+ sb.append(typeName).append('/');
+ String base = sb.toString();
+
+ for (ResourceItem item : repository.getResourceItemsOfType(resType)) {
+ results.add(base + item.getName());
+ }
+
+ if (!isSystem && resType == ResourceType.ATTR) {
+ for (ResourceItem item : repository.getResourceItemsOfType(
+ ResourceType.STYLE)) {
+ results.add(base + item.getName());
+ }
+ }
+ }
+ }
+
+ if (attributeDescriptor != null) {
+ sortAttributeChoices(attributeDescriptor, results);
+ } else {
+ Collections.sort(results);
+ }
+
+ return results.toArray(new String[results.size()]);
+ }
+
+ /**
+ * Attempts to sort the attribute values to bubble up the most likely choices to
+ * the top.
+ * <p>
+ * For example, if you are editing a style attribute, it's likely that among the
+ * resource values you would rather see @style or @android than @string.
+ * @param descriptor the descriptor that the resource values are being completed for,
+ * used to prioritize some of the resource types
+ * @param choices the set of string resource values
+ */
+ public static void sortAttributeChoices(AttributeDescriptor descriptor,
+ List<String> choices) {
+ final IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
+ Collections.sort(choices, new Comparator<String>() {
+ @Override
+ public int compare(String s1, String s2) {
+ int compare = score(attributeInfo, s1) - score(attributeInfo, s2);
+ if (compare == 0) {
+ // Sort alphabetically as a fallback
+ compare = s1.compareToIgnoreCase(s2);
+ }
+ return compare;
+ }
+ });
+ }
+
+ /** Compute a suitable sorting score for the given */
+ private static final int score(IAttributeInfo attributeInfo, String value) {
+ if (value.equals(ANDROID_PREFIX)) {
+ return -1;
+ }
+
+ for (Format format : attributeInfo.getFormats()) {
+ String type = null;
+ switch (format) {
+ case BOOLEAN:
+ type = "bool"; //$NON-NLS-1$
+ break;
+ case COLOR:
+ type = "color"; //$NON-NLS-1$
+ break;
+ case DIMENSION:
+ type = "dimen"; //$NON-NLS-1$
+ break;
+ case INTEGER:
+ type = "integer"; //$NON-NLS-1$
+ break;
+ case STRING:
+ type = "string"; //$NON-NLS-1$
+ break;
+ // default: REFERENCE, FLAG, ENUM, etc - don't have type info about individual
+ // elements to help make a decision
+ }
+
+ if (type != null) {
+ if (value.startsWith(PREFIX_RESOURCE_REF)) {
+ if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ if (value.startsWith(PREFIX_THEME_REF)) {
+ if (value.startsWith(PREFIX_THEME_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ }
+ }
+
+ // Handle a few more cases not covered by the Format metadata check
+ String type = null;
+
+ String attribute = attributeInfo.getName();
+ if (attribute.equals(ATTR_ID)) {
+ type = "id"; //$NON-NLS-1$
+ } else if (attribute.equals(ATTR_STYLE)) {
+ type = "style"; //$NON-NLS-1$
+ } else if (attribute.equals(ATTR_LAYOUT)) {
+ type = "layout"; //$NON-NLS-1$
+ } else if (attribute.equals("drawable")) { //$NON-NLS-1$
+ type = "drawable"; //$NON-NLS-1$
+ } else if (attribute.equals("entries")) { //$NON-NLS-1$
+ // Spinner
+ type = "array"; //$NON-NLS-1$
+ }
+
+ if (type != null) {
+ if (value.startsWith(PREFIX_RESOURCE_REF)) {
+ if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ if (value.startsWith(PREFIX_THEME_REF)) {
+ if (value.startsWith(PREFIX_THEME_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java
new file mode 100644
index 000000000..3d2006299
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+import org.w3c.dom.Node;
+
+/**
+ * {@link UiSeparatorAttributeNode} does not represent any real attribute.
+ * <p/>
+ * It is used to separate groups of attributes visually.
+ */
+public class UiSeparatorAttributeNode extends UiAttributeNode {
+
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor} */
+ public UiSeparatorAttributeNode(SeparatorAttributeDescriptor attrDesc,
+ UiElementNode uiParent) {
+ super(attrDesc, uiParent);
+ }
+
+ /** Returns the current value of the node. */
+ @Override
+ public String getCurrentValue() {
+ // There is no value here.
+ return null;
+ }
+
+ /**
+ * Sets whether the attribute is dirty and also notifies the editor some part's dirty
+ * flag as changed.
+ * <p/>
+ * Subclasses should set the to true as a result of user interaction with the widgets in
+ * the section and then should set to false when the commit() method completed.
+ */
+ @Override
+ public void setDirty(boolean isDirty) {
+ // This is never dirty.
+ }
+
+ /**
+ * Called once by the parent user interface to creates the necessary
+ * user interface to edit this attribute.
+ * <p/>
+ * This method can be called more than once in the life cycle of an UI node,
+ * typically when the UI is part of a master-detail tree, as pages are swapped.
+ *
+ * @param parent The composite where to create the user interface.
+ * @param managedForm The managed form owning this part.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite row = toolkit.createComposite(parent);
+
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ row.setLayoutData(twd);
+ row.setLayout(new GridLayout(3, false /* equal width */));
+
+ Label sep = toolkit.createSeparator(row, SWT.HORIZONTAL);
+ GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+ gd.widthHint = 16;
+ sep.setLayoutData(gd);
+
+ Label label = toolkit.createLabel(row, getDescriptor().getXmlLocalName());
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
+
+ sep = toolkit.createSeparator(row, SWT.HORIZONTAL);
+ sep.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+ }
+
+ /**
+ * No completion values for this UI attribute.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ return null;
+ }
+
+ /**
+ * Called when the XML is being loaded or has changed to
+ * update the value held by this user interface attribute node.
+ * <p/>
+ * The XML Node <em>may</em> be null, which denotes that the attribute is not
+ * specified in the XML model. In general, this means the "default" value of the
+ * attribute should be used.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. It's up to the
+ * UI implementation to minimize refreshes.
+ *
+ * @param xml_attribute_node
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ // No value to update.
+ }
+
+ /**
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ * <p/>
+ * Important behaviors:
+ * <ul>
+ * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before.
+ * The implemented methods must assume it is safe to modify the XML model.
+ * <li>On success, the implementation *must* call setDirty(false).
+ * <li>On failure, the implementation can fail with an exception, which
+ * is trapped and logged by the caller, or do nothing, whichever is more
+ * appropriate.
+ * </ul>
+ */
+ @Override
+ public void commit() {
+ // No value to commit.
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java
new file mode 100644
index 000000000..504ac3122
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ * <p/>
+ * The XML attribute has no default value. When unset, the text field is blank.
+ * When updating the XML, if the field is empty, the attribute will be removed
+ * from the XML element.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public class UiTextAttributeNode extends UiAbstractTextAttributeNode {
+
+ /** Text field */
+ private Text mText;
+ /** The managed form, set only once createUiControl has been called. */
+ private IManagedForm mManagedForm;
+
+ public UiTextAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+ Text text = SectionHelper.createLabelAndText(parent, managedForm.getToolkit(),
+ desc.getUiName(), getCurrentValue(),
+ DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ setTextWidget(text);
+ }
+
+ /**
+ * No completion values for this UI attribute.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ return null;
+ }
+
+ /**
+ * Sets the internal managed form.
+ * This is usually set by createUiControl.
+ */
+ protected void setManagedForm(IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ }
+
+ /**
+ * @return The managed form, set only once createUiControl has been called.
+ */
+ protected IManagedForm getManagedForm() {
+ return mManagedForm;
+ }
+
+ /* (non-java doc)
+ * Returns if the attribute node is valid, and its UI has been created.
+ */
+ @Override
+ public boolean isValid() {
+ return mText != null;
+ }
+
+ @Override
+ public String getTextWidgetValue() {
+ if (mText != null) {
+ return mText.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setTextWidgetValue(String value) {
+ if (mText != null) {
+ mText.setText(value);
+ }
+ }
+
+ /**
+ * Sets the Text widget object, and prepares it to handle modification and synchronization
+ * with the XML node.
+ * @param textWidget
+ */
+ protected final void setTextWidget(Text textWidget) {
+ mText = textWidget;
+
+ if (textWidget != null) {
+ // Sets the with hint for the text field. Derived classes can always override it.
+ // This helps the grid layout to resize correctly on smaller screen sizes.
+ Object data = textWidget.getLayoutData();
+ if (data == null) {
+ } else if (data instanceof GridData) {
+ ((GridData)data).widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ } else if (data instanceof TableWrapData) {
+ ((TableWrapData)data).maxWidth = 100;
+ }
+
+ mText.addModifyListener(new ModifyListener() {
+ /**
+ * Sent when the text is modified, whether by the user via manual
+ * input or programmatic input via setText().
+ * <p/>
+ * Simply mark the attribute as dirty if it really changed.
+ * The container SectionPart will collect these flag and manage them.
+ */
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (!isInInternalTextModification() &&
+ !isDirty() &&
+ mText != null &&
+ getCurrentValue() != null &&
+ !mText.getText().equals(getCurrentValue())) {
+ setDirty(true);
+ }
+ }
+ });
+
+ // Remove self-reference when the widget is disposed
+ mText.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ mText = null;
+ }
+ });
+ }
+
+ onAddValidators(mText);
+ }
+
+ /**
+ * Called after the text widget as been created.
+ * <p/>
+ * Derived classes typically want to:
+ * <li> Create a new {@link ModifyListener} and attach it to the given {@link Text} widget.
+ * <li> In the modify listener, call getManagedForm().getMessageManager().addMessage()
+ * and getManagedForm().getMessageManager().removeMessage() as necessary.
+ * <li> Call removeMessage in a new text.addDisposeListener.
+ * <li> Call the validator once to setup the initial messages as needed.
+ * <p/>
+ * The base implementation does nothing.
+ *
+ * @param text The {@link Text} widget to validate.
+ */
+ protected void onAddValidators(Text text) {
+ }
+
+ /**
+ * Returns the text widget.
+ */
+ protected final Text getTextWidget() {
+ return mText;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java
new file mode 100644
index 000000000..33fa9fc99
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+/**
+ * Represents an XML element value in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ */
+public class UiTextValueNode extends UiTextAttributeNode {
+
+ public UiTextValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /**
+ * Updates the current text field's value when the XML has changed.
+ * <p/>
+ * The caller doesn't really know if value of the element has changed,
+ * so it will call this to refresh the value anyway. The value
+ * is only set if it has changed.
+ * <p/>
+ * This also resets the "dirty" flag.
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ setCurrentValue(DEFAULT_VALUE);
+
+ // The argument xml_attribute_node is not used here. It should always be
+ // null since this is not an attribute. What we want is the "text value" of
+ // the parent element, which is actually the first text node of the element.
+
+ UiElementNode parent = getUiParent();
+ if (parent != null) {
+ Node xml_node = parent.getXmlNode();
+ if (xml_node != null) {
+ for (Node xml_child = xml_node.getFirstChild();
+ xml_child != null;
+ xml_child = xml_child.getNextSibling()) {
+ if (xml_child.getNodeType() == Node.TEXT_NODE) {
+ setCurrentValue(xml_child.getNodeValue());
+ break;
+ }
+ }
+ }
+ }
+
+ if (isValid() && !getTextWidgetValue().equals(getCurrentValue())) {
+ try {
+ setInInternalTextModification(true);
+ setTextWidgetValue(getCurrentValue());
+ setDirty(false);
+ } finally {
+ setInInternalTextModification(false);
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified "attributes" must be committed (i.e. written) to the XML model.
+ */
+ @Override
+ public void commit() {
+ UiElementNode parent = getUiParent();
+ if (parent != null && isValid() && isDirty()) {
+ // Get (or create) the underlying XML element node that contains the value.
+ Node element = parent.prepareCommit();
+ if (element != null) {
+ String value = getTextWidgetValue();
+
+ // Try to find an existing text child to update.
+ boolean updated = false;
+
+ for (Node xml_child = element.getFirstChild();
+ xml_child != null;
+ xml_child = xml_child.getNextSibling()) {
+ if (xml_child.getNodeType() == Node.TEXT_NODE) {
+ xml_child.setNodeValue(value);
+ updated = true;
+ break;
+ }
+ }
+
+ // If we didn't find a text child to update, we need to create one.
+ if (!updated) {
+ Document doc = element.getOwnerDocument();
+ if (doc != null) {
+ Text text = doc.createTextNode(value);
+ element.appendChild(text);
+ }
+ }
+
+ setCurrentValue(value);
+ }
+ }
+ setDirty(false);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java
new file mode 100644
index 000000000..d0ee92ca1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TYPE;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.TAG_ITEM;
+import static com.android.SdkConstants.TAG_STYLE;
+import static com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor.ATTRIBUTE_ICON_FILENAME;
+import static com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.DESCRIPTOR_LAYOUT;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.jface.text.contentassist.CompletionProposal;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Content Assist Processor for /res/values and /res/drawable XML files
+ * <p>
+ * Further enhancements:
+ * <ul>
+ * <li> Complete prefixes in the style element itself for the name attribute
+ * <li> Complete parent names
+ * </ul>
+ */
+@VisibleForTesting
+public class ValuesContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for ResourcesContentAssist
+ */
+ public ValuesContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_RESOURCES);
+ }
+
+ @Override
+ protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset,
+ String parentTagName, String attributeName, Node node, String wordPrefix,
+ boolean skipEndTag, int replaceLength) {
+ super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node,
+ wordPrefix, skipEndTag, replaceLength);
+
+ if (parentTagName.equals(TAG_ITEM) && ATTR_NAME.equals(attributeName)) {
+
+ // Special case: the user is code completing inside
+ // <style><item name="^"/></style>
+ // In this case, ALL attributes are valid so we need to synthesize
+ // a choice list from all the layout descriptors
+
+ // Add in android: as a completion item?
+ if (startsWith(ANDROID_NS_NAME_PREFIX, wordPrefix)) {
+ proposals.add(new CompletionProposal(ANDROID_NS_NAME_PREFIX,
+ offset - wordPrefix.length(), // replacementOffset
+ wordPrefix.length() + replaceLength, // replacementLength
+ ANDROID_NS_NAME_PREFIX.length(), // cursorPosition
+ IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME),
+ null, null, null));
+ }
+
+
+ String attributePrefix = wordPrefix;
+ if (startsWith(attributePrefix, ANDROID_NS_NAME_PREFIX)) {
+ attributePrefix = attributePrefix.substring(ANDROID_NS_NAME_PREFIX.length());
+ }
+
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ IDescriptorProvider descriptorProvider =
+ data.getDescriptorProvider(
+ AndroidTargetData.DESCRIPTOR_LAYOUT);
+ if (descriptorProvider != null) {
+ ElementDescriptor[] rootElementDescriptors =
+ descriptorProvider.getRootElementDescriptors();
+ Map<String, AttributeDescriptor> matches =
+ new HashMap<String, AttributeDescriptor>(180);
+ for (ElementDescriptor elementDesc : rootElementDescriptors) {
+ for (AttributeDescriptor desc : elementDesc.getAttributes()) {
+ if (desc instanceof SeparatorAttributeDescriptor) {
+ continue;
+ }
+ String name = desc.getXmlLocalName();
+ if (startsWith(name, attributePrefix)) {
+ matches.put(name, desc);
+ }
+ }
+ }
+
+ List<AttributeDescriptor> sorted =
+ new ArrayList<AttributeDescriptor>(matches.size());
+ sorted.addAll(matches.values());
+ Collections.sort(sorted);
+ char needTag = 0;
+ addMatchingProposals(proposals, sorted.toArray(), offset, node, wordPrefix,
+ needTag, true /* isAttribute */, false /* isNew */,
+ skipEndTag /* skipEndTag */, replaceLength);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void computeTextValues(List<ICompletionProposal> proposals, int offset,
+ Node parentNode, Node currentNode, UiElementNode uiParent,
+ String prefix) {
+ super.computeTextValues(proposals, offset, parentNode, currentNode, uiParent,
+ prefix);
+
+ if (parentNode.getNodeName().equals(TAG_ITEM) &&
+ parentNode.getParentNode() != null &&
+ TAG_STYLE.equals(parentNode.getParentNode().getNodeName())) {
+
+ // Special case: the user is code completing inside
+ // <style><item name="android:foo"/>|</style>
+ // In this case, we need to find the right AttributeDescriptor
+ // for the given attribute and offer its values
+
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ IDescriptorProvider descriptorProvider =
+ data.getDescriptorProvider(DESCRIPTOR_LAYOUT);
+ if (descriptorProvider != null) {
+
+ Element element = (Element) parentNode;
+ String attrName = element.getAttribute(ATTR_NAME);
+ int pos = attrName.indexOf(':');
+ if (pos >= 0) {
+ attrName = attrName.substring(pos + 1);
+ }
+
+ // Search for an attribute match
+ ElementDescriptor[] rootElementDescriptors =
+ descriptorProvider.getRootElementDescriptors();
+ for (ElementDescriptor elementDesc : rootElementDescriptors) {
+ for (AttributeDescriptor desc : elementDesc.getAttributes()) {
+ if (desc.getXmlLocalName().equals(attrName)) {
+ // Make a ui parent node such that we can attach our
+ // newfound attribute node to something (the code we delegate
+ // to for looking up attribute completions will look at the
+ // parent node and ask for its editor etc.)
+ if (uiParent == null) {
+ DocumentDescriptor documentDescriptor =
+ data.getLayoutDescriptors().getDescriptor();
+ uiParent = documentDescriptor.createUiNode();
+ uiParent.setEditor(mEditor);
+ }
+
+ UiAttributeNode currAttrNode = desc.createUiNode(uiParent);
+ AttribInfo attrInfo = new AttribInfo();
+ Object[] values = getAttributeValueChoices(currAttrNode, attrInfo,
+ prefix);
+ char needTag = attrInfo.needTag;
+ if (attrInfo.correctedPrefix != null) {
+ prefix = attrInfo.correctedPrefix;
+ }
+ boolean isAttribute = true;
+ boolean isNew = false;
+ int replaceLength = computeTextReplaceLength(currentNode, offset);
+ addMatchingProposals(proposals, values, offset, currentNode,
+ prefix, needTag, isAttribute, isNew,
+ false /* skipEndTag */, replaceLength);
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (parentNode.getNodeName().equals(TAG_ITEM)) {
+ // Completing text content inside an <item> tag: offer @resource completion.
+ if (prefix.startsWith(PREFIX_RESOURCE_REF) || prefix.trim().length() == 0) {
+ String[] choices = UiResourceAttributeNode.computeResourceStringMatches(
+ mEditor, null /*attributeDescriptor*/, prefix);
+ if (choices == null || choices.length == 0) {
+ return;
+ }
+
+ // If the parent item tag specifies a type, filter the results
+ Node typeNode = parentNode.getAttributes().getNamedItem(ATTR_TYPE);
+ if (typeNode != null) {
+ String value = typeNode.getNodeValue();
+ List<String> filtered = new ArrayList<String>();
+ for (String s : choices) {
+ if (s.startsWith(ANDROID_PREFIX) ||
+ s.startsWith(PREFIX_RESOURCE_REF+ value)) {
+ filtered.add(s);
+ }
+ }
+ if (filtered.size() > 0) {
+ choices = filtered.toArray(new String[filtered.size()]);
+ }
+ }
+
+ int replaceLength = computeTextReplaceLength(currentNode, offset);
+ addMatchingProposals(proposals, choices, offset, currentNode,
+ prefix, (char) 0 /*needTag*/, true /* isAttribute */, false /*isNew*/,
+ false /* skipEndTag*/,
+ replaceLength);
+
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesEditorDelegate.java
new file mode 100644
index 000000000..10f105f85
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesEditorDelegate.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors;
+import com.android.resources.ResourceFolderType;
+import com.android.xml.AndroidXPathFactory;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.ui.PartInitException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Multi-page form editor for /res/values XML files.
+ */
+public class ValuesEditorDelegate extends CommonXmlDelegate {
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public ValuesEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.VALUES == type) {
+ return new ValuesEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ AdtConstants.EDITORS_NAMESPACE + ".resources.ResourcesEditor"; //$NON-NLS-1$
+
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ private ValuesEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new ValuesContentAssist());
+ editor.addDefaultTargetListener();
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ public void delegateCreateFormPages() {
+ try {
+ getEditor().addPage(new ValuesTreePage(getEditor()));
+ } catch (PartInitException e) {
+ AdtPlugin.log(IStatus.ERROR, "Error creating nested page"); //$NON-NLS-1$
+ AdtPlugin.getDefault().getLog().log(e.getStatus());
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ public void delegateXmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ delegateInitUiRootNode(false /*force*/);
+
+ getUiRootNode().setXmlDocument(xml_doc);
+ if (xml_doc != null) {
+ ElementDescriptor resources_desc =
+ ValuesDescriptors.getInstance().getElementDescriptor();
+ try {
+ XPath xpath = AndroidXPathFactory.newXPath();
+ Node node = (Node) xpath.evaluate("/" + resources_desc.getXmlName(), //$NON-NLS-1$
+ xml_doc,
+ XPathConstants.NODE);
+ // Node can be null _or_ it must be the element we searched for.
+ assert node == null || node.getNodeName().equals(resources_desc.getXmlName());
+
+ // Refresh the manifest UI node and all its descendants
+ getUiRootNode().loadFromXmlNode(node);
+ } catch (XPathExpressionException e) {
+ AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$
+ resources_desc.getXmlName());
+ }
+ }
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiRootNode is recreated even if it already exists.
+ */
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (getUiRootNode() == null || force) {
+ ElementDescriptor resources_desc =
+ ValuesDescriptors.getInstance().getElementDescriptor();
+ setUiRootNode(resources_desc.createUiNode());
+ getUiRootNode().setEditor(getEditor());
+
+ onDescriptorsChanged();
+ }
+ }
+
+ // ---- Local methods ----
+
+ private void onDescriptorsChanged() {
+ // nothing to be done, as the descriptor are static for now.
+ // FIXME Update when the descriptors are not static
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesTreePage.java
new file mode 100644
index 000000000..224eb7301
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesTreePage.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values;
+
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.FlagManager;
+import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Page for instrumentation settings, part of the AndroidManifest form editor.
+ */
+public final class ValuesTreePage extends FormPage implements IPageImageProvider {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "res_tree_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ CommonXmlEditor mEditor;
+
+ public ValuesTreePage(CommonXmlEditor editor) {
+ super(editor, PAGE_ID, "Resources"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ @Override
+ public Image getPageImage() {
+ // See if we should use a flag icon if this is a language-specific configuration
+ IFile file = mEditor.getInputFile();
+ if (file != null) {
+ IContainer parent = file.getParent();
+ if (parent != null) {
+ Image flag = FlagManager.get().getFlagForFolderName(parent.getName());
+ if (flag != null) {
+ return flag;
+ }
+ }
+ }
+
+ return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+
+ String configText = null;
+ IEditorInput input = mEditor.getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ IFile iFile = fileInput.getFile();
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(iFile);
+ if (resFolder != null) {
+ configText = resFolder.getConfiguration().toDisplayString();
+ }
+ }
+
+ if (configText != null) {
+ form.setText(String.format("Android Resources (%1$s)", configText));
+ } else {
+ form.setText("Android Resources");
+ }
+
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode resources = mEditor.getUiRootNode();
+ UiTreeBlock block = new UiTreeBlock(mEditor, resources,
+ true /* autoCreateRoot */,
+ null /* no element filters */,
+ "Resources Elements",
+ "List of all resources elements in this XML file.");
+ block.createContent(managedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ColorValueDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ColorValueDescriptor.java
new file mode 100644
index 000000000..012785020
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ColorValueDescriptor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.values.uimodel.UiColorValueNode;
+
+/**
+ * Describes a Color XML element value displayed by an {@link UiColorValueNode}.
+ */
+public final class ColorValueDescriptor extends TextValueDescriptor {
+
+ public ColorValueDescriptor(String uiName, String tooltip) {
+ super(uiName, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiColorValueNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ItemElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ItemElementDescriptor.java
new file mode 100644
index 000000000..58ed36e45
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ItemElementDescriptor.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values.descriptors;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.values.uimodel.UiItemElementNode;
+
+/**
+ * {@link ItemElementDescriptor} is a special version of {@link ElementDescriptor} that
+ * uses a specialized {@link UiItemElementNode} for display.
+ */
+public class ItemElementDescriptor extends ElementDescriptor {
+
+ /**
+ * Constructs a new {@link ItemElementDescriptor} based on its XML name, UI name,
+ * tooltip, SDK url, attributes list, children list and mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ItemElementDescriptor(String xml_name, String ui_name,
+ String tooltip, String sdk_url, AttributeDescriptor[] attributes,
+ ElementDescriptor[] children, boolean mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ }
+
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiItemElementNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ValuesDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ValuesDescriptors.java
new file mode 100644
index 000000000..724e01932
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/descriptors/ValuesDescriptors.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values.descriptors;
+
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TYPE;
+import static com.android.SdkConstants.TAG_COLOR;
+import static com.android.SdkConstants.TAG_DIMEN;
+import static com.android.SdkConstants.TAG_DRAWABLE;
+import static com.android.SdkConstants.TAG_INTEGER_ARRAY;
+import static com.android.SdkConstants.TAG_ITEM;
+import static com.android.SdkConstants.TAG_PLURALS;
+import static com.android.SdkConstants.TAG_RESOURCES;
+import static com.android.SdkConstants.TAG_STRING;
+import static com.android.SdkConstants.TAG_STRING_ARRAY;
+import static com.android.SdkConstants.TAG_STYLE;
+
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.EnumAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.FlagAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
+import com.android.resources.ResourceType;
+
+import java.util.EnumSet;
+
+
+/**
+ * Complete description of the structure for resources XML files (under res/values/)
+ */
+public final class ValuesDescriptors implements IDescriptorProvider {
+ private static final ValuesDescriptors sThis = new ValuesDescriptors();
+
+ /** The {@link ElementDescriptor} for the root Resources element. */
+ public final ElementDescriptor mResourcesElement;
+
+ public static ValuesDescriptors getInstance() {
+ return sThis;
+ }
+
+ /*
+ * @see com.android.ide.eclipse.editors.descriptors.IDescriptorProvider#getRootElementDescriptors()
+ */
+ @Override
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return new ElementDescriptor[] { mResourcesElement };
+ }
+
+ @Override
+ public ElementDescriptor getDescriptor() {
+ return mResourcesElement;
+ }
+
+ public ElementDescriptor getElementDescriptor() {
+ return mResourcesElement;
+ }
+
+ private ValuesDescriptors() {
+
+ // Common attributes used in many placed
+
+ // Elements
+
+ AttributeInfo nameAttrInfo = new AttributeInfo(ATTR_NAME, Format.STRING_SET);
+
+ ElementDescriptor color_element = new ElementDescriptor(
+ TAG_COLOR,
+ "Color",
+ "A @color@ value specifies an RGB value with an alpha channel, which can be used in various places such as specifying a solid color for a Drawable or the color to use for text. It always begins with a # character and then is followed by the alpha-red-green-blue information in one of the following formats: #RGB, #ARGB, #RRGGBB or #AARRGGBB.",
+ "http://code.google.com/android/reference/available-resources.html#colorvals", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo),
+ new ColorValueDescriptor(
+ "Value*",
+ "A mandatory color value.")
+ .setTooltip("The mandatory name used in referring to this color.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor string_element = new ElementDescriptor(
+ TAG_STRING,
+ "String",
+ "@Strings@, with optional simple formatting, can be stored and retrieved as resources. You can add formatting to your string by using three standard HTML tags: b, i, and u. If you use an apostrophe or a quote in your string, you must either escape it or enclose the whole string in the other kind of enclosing quotes.",
+ "http://code.google.com/android/reference/available-resources.html#stringresources", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this string."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory string value.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor item_element = new ItemElementDescriptor(
+ TAG_ITEM,
+ "Item",
+ null, // TODO find javadoc
+ null, // TODO find link to javadoc
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this resource."),
+ new ListAttributeDescriptor(ATTR_TYPE,
+ null /* nsUri */,
+ new AttributeInfo(ATTR_TYPE,
+ EnumSet.of(Format.STRING, Format.ENUM)
+ ).setEnumValues(ResourceType.getNames())
+ ).setTooltip("The mandatory type of this resource."),
+ new FlagAttributeDescriptor("format", //$NON-NLS-1$
+ null /* nsUri */,
+ new AttributeInfo("format",
+ EnumSet.of(Format.STRING, Format.FLAG)
+ ).setFlagValues(
+ new String[] {
+ "boolean", //$NON-NLS-1$
+ TAG_COLOR,
+ "dimension", //$NON-NLS-1$
+ "float", //$NON-NLS-1$
+ "fraction", //$NON-NLS-1$
+ "integer", //$NON-NLS-1$
+ "reference", //$NON-NLS-1$
+ "string" //$NON-NLS-1$
+ } )
+ ).setTooltip("The optional format of this resource."),
+ new TextValueDescriptor(
+ "Value",
+ "A standard string, hex color value, or reference to any other resource type.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor drawable_element = new ElementDescriptor(
+ TAG_DRAWABLE,
+ "Drawable",
+ "A @drawable@ defines a rectangle of color. Android accepts color values written in various web-style formats -- a hexadecimal constant in any of the following forms: #RGB, #ARGB, #RRGGBB, #AARRGGBB. Zero in the alpha channel means transparent. The default value is opaque.",
+ "http://code.google.com/android/reference/available-resources.html#colordrawableresources", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this drawable."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory color value in the form #RGB, #ARGB, #RRGGBB or #AARRGGBB.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor dimen_element = new ElementDescriptor(
+ TAG_DIMEN,
+ "Dimension",
+ "You can create common dimensions to use for various screen elements by defining @dimension@ values in XML. A dimension resource is a number followed by a unit of measurement. Supported units are px (pixels), in (inches), mm (millimeters), pt (points at 72 DPI), dp (density-independent pixels) and sp (scale-independent pixels)",
+ "http://code.google.com/android/reference/available-resources.html#dimension", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this dimension."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory dimension value is a number followed by a unit of measurement. For example: 10px, 2in, 5sp.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor style_element = new ElementDescriptor(
+ TAG_STYLE,
+ "Style/Theme",
+ "Both @styles and themes@ are defined in a style block containing one or more string or numerical values (typically color values), or references to other resources (drawables and so on).",
+ "http://code.google.com/android/reference/available-resources.html#stylesandthemes", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this theme."),
+ new TextAttributeDescriptor("parent", //$NON-NLS-1$
+ null /* nsUri */,
+ new AttributeInfo("parent", //$NON-NLS-1$
+ Format.STRING_SET))
+ .setTooltip("An optional parent theme. All values from the specified theme will be inherited into this theme. Any values with identical names that you specify will override inherited values."),
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ TAG_ITEM,
+ "Item",
+ "A value to use in this @theme@. It can be a standard string, a hex color value, or a reference to any other resource type.",
+ "http://code.google.com/android/reference/available-resources.html#stylesandthemes", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this item."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory standard string, hex color value, or reference to any other resource type.")
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ ElementDescriptor string_array_element = new ElementDescriptor(
+ TAG_STRING_ARRAY,
+ "String Array",
+ "An array of strings. Strings are added as underlying item elements to the array.",
+ null, // tooltips
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this string array."),
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ TAG_ITEM,
+ "Item",
+ "A string value to use in this string array.",
+ null, // tooltip
+ new AttributeDescriptor[] {
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory string.")
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ ElementDescriptor plurals_element = new ElementDescriptor(
+ TAG_PLURALS,
+ "Quantity Strings (Plurals)",
+ "A quantity string",
+ null, // tooltips
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("A name for the pair of strings. This name will be used as the resource ID."),
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ TAG_ITEM,
+ "Item",
+ "A plural or singular string",
+ null, // tooltip
+ new AttributeDescriptor[] {
+ new EnumAttributeDescriptor(
+ "quantity", "Quantity", null,
+ "A keyword value indicating when this string should be used",
+ new AttributeInfo("quantity", Format.ENUM_SET)
+ .setEnumValues(new String[] {
+ "zero", //$NON-NLS-1$
+ "one", //$NON-NLS-1$
+ "two", //$NON-NLS-1$
+ "few", //$NON-NLS-1$
+ "many", //$NON-NLS-1$
+ "other" //$NON-NLS-1$
+ }))
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ ElementDescriptor integer_array_element = new ElementDescriptor(
+ TAG_INTEGER_ARRAY,
+ "Integer Array",
+ "An array of integers. Integers are added as underlying item elements to the array.",
+ null, // tooltips
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(ATTR_NAME,
+ null /* nsUri */,
+ nameAttrInfo)
+ .setTooltip("The mandatory name used in referring to this integer array.")
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ TAG_ITEM,
+ "Item",
+ "An integer value to use in this integer array.",
+ null, // tooltip
+ new AttributeDescriptor[] {
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory integer.")
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ mResourcesElement = new ElementDescriptor(
+ TAG_RESOURCES,
+ "Resources",
+ null,
+ "http://code.google.com/android/reference/available-resources.html", //$NON-NLS-1$
+ null, // no attributes
+ new ElementDescriptor[] {
+ string_element,
+ color_element,
+ dimen_element,
+ drawable_element,
+ style_element,
+ item_element,
+ string_array_element,
+ integer_array_element,
+ plurals_element,
+ },
+ true /* mandatory */);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiColorValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiColorValueNode.java
new file mode 100644
index 000000000..9c84b36f9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiColorValueNode.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values.uimodel;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextValueNode;
+
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.regex.Pattern;
+
+/**
+ * Displays and edits a color XML element value with a custom validator.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public class UiColorValueNode extends UiTextValueNode {
+
+ /** Accepted RGBA formats are one of #RGB, #ARGB, #RRGGBB or #AARRGGBB. */
+ private static final Pattern RGBA_REGEXP = Pattern.compile(
+ "#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})"); //$NON-NLS-1$
+
+ public UiColorValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ *
+ * Add a modify listener that will check colors have the proper format,
+ * that is one of #RGB, #ARGB, #RRGGBB or #AARRGGBB.
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ String color = text.getText();
+ if (RGBA_REGEXP.matcher(color).matches()) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ } else {
+ getManagedForm().getMessageManager().addMessage(text,
+ "Accepted color formats are one of #RGB, #ARGB, #RRGGBB or #AARRGGBB.",
+ null /* data */, IMessageProvider.ERROR, text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiItemElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiItemElementNode.java
new file mode 100644
index 000000000..88ac3e141
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/uimodel/UiItemElementNode.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.values.uimodel;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ItemElementDescriptor;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * {@link UiItemElementNode} is a special version of {@link UiElementNode} that
+ * customizes the element display to include the item type attribute if present.
+ */
+public class UiItemElementNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiElementNode} described by a given {@link ItemElementDescriptor}.
+ *
+ * @param elementDescriptor The {@link ItemElementDescriptor} for the XML node. Cannot be null.
+ */
+ public UiItemElementNode(ItemElementDescriptor elementDescriptor) {
+ super(elementDescriptor);
+ }
+
+ @Override
+ public String getShortDescription() {
+ Node xmlNode = getXmlNode();
+ if (xmlNode != null && xmlNode instanceof Element && xmlNode.hasAttributes()) {
+
+ Element elem = (Element) xmlNode;
+ String type = elem.getAttribute(SdkConstants.ATTR_TYPE);
+ String name = elem.getAttribute(SdkConstants.ATTR_NAME);
+ if (type != null && name != null && type.length() > 0 && name.length() > 0) {
+ type = AdtUtils.capitalize(type);
+ return String.format("%1$s (%2$s %3$s)", name, type, getDescriptor().getUiName());
+ }
+ }
+
+ return super.getShortDescription();
+ }
+}