aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout')
-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
157 files changed, 63020 insertions, 0 deletions
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;
+ }
+}