1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.common.layout; 18 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; 24 import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL; 25 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; 26 27 import com.android.ide.common.api.DropFeedback; 28 import com.android.ide.common.api.IAttributeInfo.Format; 29 import com.android.ide.common.api.IDragElement; 30 import com.android.ide.common.api.IMenuCallback; 31 import com.android.ide.common.api.INode; 32 import com.android.ide.common.api.IViewRule; 33 import com.android.ide.common.api.Point; 34 import com.android.ide.common.api.Rect; 35 import com.android.ide.common.api.RuleAction; 36 import com.android.ide.common.api.RuleAction.NestedAction; 37 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.Collections; 41 import java.util.List; 42 import java.util.Locale; 43 44 /** Test the {@link LinearLayoutRule} */ 45 public class LinearLayoutRuleTest extends LayoutTestBase { 46 // Utility for other tests 47 protected void dragIntoEmpty(Rect dragBounds) { 48 boolean haveBounds = dragBounds.isValid(); 49 50 IViewRule rule = new LinearLayoutRule(); 51 52 INode targetNode = TestNode.create("android.widget.LinearLayout").id( 53 "@+id/LinearLayout01").bounds(new Rect(0, 0, 240, 480)); 54 Point dropPoint = new Point(10, 5); 55 56 IDragElement[] elements = TestDragElement.create(TestDragElement.create( 57 "android.widget.Button", dragBounds).id("@+id/Button01")); 58 59 // Enter target 60 DropFeedback feedback = rule.onDropEnter(targetNode, null/*targetView*/, elements); 61 assertNotNull(feedback); 62 assertFalse(feedback.invalidTarget); 63 assertNotNull(feedback.painter); 64 65 feedback = rule.onDropMove(targetNode, elements, feedback, dropPoint); 66 assertNotNull(feedback); 67 assertFalse(feedback.invalidTarget); 68 69 // Paint feedback and make sure it's what we expect 70 TestGraphics graphics = new TestGraphics(); 71 assertNotNull(feedback.painter); 72 feedback.painter.paint(graphics, targetNode, feedback); 73 assertEquals( 74 // Expect to see a recipient rectangle around the bounds of the 75 // LinearLayout, 76 // as well as a single vertical line as a drop preview located 77 // along the left 78 // edge (for this horizontal linear layout) showing insert 79 // position at index 0, 80 // and finally a rectangle for the bounds of the inserted button 81 // centered over 82 // the middle 83 "[useStyle(DROP_RECIPIENT), " 84 + 85 // Bounds rectangle 86 "drawRect(Rect[0,0,240,480]), " 87 + "useStyle(DROP_ZONE), drawLine(1,0,1,480), " 88 + "useStyle(DROP_ZONE_ACTIVE), " + "useStyle(DROP_PREVIEW), " + 89 // Insert position line 90 "drawLine(1,0,1,480)" + (haveBounds ? 91 // Outline of dragged node centered over position line 92 ", useStyle(DROP_PREVIEW), " + "drawRect(1,0,101,80)" 93 // Nothing when we don't have bounds 94 : "") + "]", graphics.getDrawn().toString()); 95 96 // Attempt a drop 97 assertEquals(0, targetNode.getChildren().length); 98 rule.onDropped(targetNode, elements, feedback, dropPoint); 99 assertEquals(1, targetNode.getChildren().length); 100 assertEquals("@+id/Button01", targetNode.getChildren()[0].getStringAttr( 101 ANDROID_URI, ATTR_ID)); 102 } 103 104 // Utility for other tests 105 protected INode dragInto(boolean vertical, Rect dragBounds, Point dragPoint, 106 int insertIndex, int currentIndex, 107 String... graphicsFragments) { 108 INode linearLayout = TestNode.create("android.widget.LinearLayout").id( 109 "@+id/LinearLayout01").bounds(new Rect(0, 0, 240, 480)).set(ANDROID_URI, 110 ATTR_ORIENTATION, 111 vertical ? VALUE_VERTICAL : VALUE_HORIZONTAL) 112 .add( 113 TestNode.create("android.widget.Button").id("@+id/Button01").bounds( 114 new Rect(0, 0, 100, 80)), 115 TestNode.create("android.widget.Button").id("@+id/Button02").bounds( 116 new Rect(0, 100, 100, 80)), 117 TestNode.create("android.widget.Button").id("@+id/Button03").bounds( 118 new Rect(0, 200, 100, 80)), 119 TestNode.create("android.widget.Button").id("@+id/Button04").bounds( 120 new Rect(0, 300, 100, 80))); 121 122 return super.dragInto(new LinearLayoutRule(), linearLayout, dragBounds, dragPoint, null, 123 insertIndex, currentIndex, graphicsFragments); 124 } 125 126 // Check that the context menu registers the expected menu items 127 public void testContextMenu() { 128 LinearLayoutRule rule = new LinearLayoutRule(); 129 initialize(rule, "android.widget.LinearLayout"); 130 INode node = TestNode.create("android.widget.Button").id("@+id/Button012"); 131 132 List<RuleAction> contextMenu = new ArrayList<RuleAction>(); 133 rule.addContextMenuActions(contextMenu, node); 134 assertEquals(6, contextMenu.size()); 135 assertEquals("Edit ID...", contextMenu.get(0).getTitle()); 136 assertTrue(contextMenu.get(1) instanceof RuleAction.Separator); 137 assertEquals("Layout Width", contextMenu.get(2).getTitle()); 138 assertEquals("Layout Height", contextMenu.get(3).getTitle()); 139 assertTrue(contextMenu.get(4) instanceof RuleAction.Separator); 140 assertEquals("Other Properties", contextMenu.get(5).getTitle()); 141 142 RuleAction propertiesMenu = contextMenu.get(5); 143 assertTrue(propertiesMenu.getClass().getName(), 144 propertiesMenu instanceof NestedAction); 145 } 146 147 public void testContextMenuCustom() { 148 LinearLayoutRule rule = new LinearLayoutRule(); 149 initialize(rule, "android.widget.LinearLayout"); 150 INode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout") 151 .set(ANDROID_URI, ATTR_LAYOUT_WIDTH, "42dip") 152 .set(ANDROID_URI, ATTR_LAYOUT_HEIGHT, "50sp"); 153 154 List<RuleAction> contextMenu = new ArrayList<RuleAction>(); 155 rule.addContextMenuActions(contextMenu, node); 156 assertEquals(6, contextMenu.size()); 157 assertEquals("Layout Width", contextMenu.get(2).getTitle()); 158 RuleAction menuAction = contextMenu.get(2); 159 assertTrue(menuAction instanceof RuleAction.Choices); 160 RuleAction.Choices choices = (RuleAction.Choices) menuAction; 161 List<String> titles = choices.getTitles(); 162 List<String> ids = choices.getIds(); 163 assertEquals("Wrap Content", titles.get(0)); 164 assertEquals("wrap_content", ids.get(0)); 165 assertEquals("Match Parent", titles.get(1)); 166 assertEquals("match_parent", ids.get(1)); 167 assertEquals("42dip", titles.get(2)); 168 assertEquals("42dip", ids.get(2)); 169 assertEquals("42dip", choices.getCurrent()); 170 } 171 172 // Check that the context menu manipulates the orientation attribute 173 public void testOrientation() { 174 LinearLayoutRule rule = new LinearLayoutRule(); 175 initialize(rule, "android.widget.LinearLayout"); 176 TestNode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout012"); 177 node.putAttributeInfo(ANDROID_URI, "orientation", 178 new TestAttributeInfo(ATTR_ORIENTATION, new Format[] { Format.ENUM }, 179 "android.widget.LinearLayout", 180 new String[] {"horizontal", "vertical"}, null, null)); 181 182 assertNull(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION)); 183 184 List<RuleAction> contextMenu = new ArrayList<RuleAction>(); 185 rule.addContextMenuActions(contextMenu, node); 186 assertEquals(7, contextMenu.size()); 187 RuleAction orientationAction = contextMenu.get(1); 188 assertEquals("Orientation", orientationAction.getTitle()); 189 190 assertTrue(orientationAction.getClass().getName(), 191 orientationAction instanceof RuleAction.Choices); 192 193 RuleAction.Choices choices = (RuleAction.Choices) orientationAction; 194 IMenuCallback callback = choices.getCallback(); 195 callback.action(orientationAction, Collections.singletonList(node), VALUE_VERTICAL, true); 196 197 String orientation = node.getStringAttr(ANDROID_URI, 198 ATTR_ORIENTATION); 199 assertEquals(VALUE_VERTICAL, orientation); 200 callback.action(orientationAction, Collections.singletonList(node), VALUE_HORIZONTAL, 201 true); 202 orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION); 203 assertEquals(VALUE_HORIZONTAL, orientation); 204 } 205 206 // Check that the context menu manipulates the orientation attribute 207 public void testProperties() { 208 LinearLayoutRule rule = new LinearLayoutRule(); 209 initialize(rule, "android.widget.LinearLayout"); 210 TestNode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout012"); 211 node.putAttributeInfo(ANDROID_URI, "orientation", 212 new TestAttributeInfo(ATTR_ORIENTATION, new Format[] { Format.ENUM }, 213 "android.widget.LinearLayout", 214 new String[] {"horizontal", "vertical"}, null, null)); 215 node.setAttributeSources(Arrays.asList("android.widget.LinearLayout", 216 "android.view.ViewGroup", "android.view.View")); 217 node.putAttributeInfo(ANDROID_URI, "gravity", 218 new TestAttributeInfo("gravity", new Format[] { Format.INTEGER }, 219 "android.widget.LinearLayout", null, null, null)); 220 221 222 assertNull(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION)); 223 224 List<RuleAction> contextMenu = new ArrayList<RuleAction>(); 225 rule.addContextMenuActions(contextMenu, node); 226 assertEquals(8, contextMenu.size()); 227 228 assertEquals("Orientation", contextMenu.get(1).getTitle()); 229 assertEquals("Edit Gravity...", contextMenu.get(2).getTitle()); 230 231 assertEquals("Other Properties", contextMenu.get(7).getTitle()); 232 233 RuleAction propertiesMenu = contextMenu.get(7); 234 assertTrue(propertiesMenu.getClass().getName(), 235 propertiesMenu instanceof NestedAction); 236 NestedAction nested = (NestedAction) propertiesMenu; 237 List<RuleAction> nestedActions = nested.getNestedActions(node); 238 assertEquals(9, nestedActions.size()); 239 assertEquals("Recent", nestedActions.get(0).getTitle()); 240 assertTrue(nestedActions.get(1) instanceof RuleAction.Separator); 241 assertEquals("Defined by LinearLayout", nestedActions.get(2).getTitle()); 242 assertEquals("Inherited from ViewGroup", nestedActions.get(3).getTitle()); 243 assertEquals("Inherited from View", nestedActions.get(4).getTitle()); 244 assertTrue(nestedActions.get(5) instanceof RuleAction.Separator); 245 assertEquals("Layout Parameters", nestedActions.get(6).getTitle()); 246 assertTrue(nestedActions.get(7) instanceof RuleAction.Separator); 247 assertEquals("All By Name", nestedActions.get(8).getTitle()); 248 249 BaseViewRule.editedProperty(ATTR_ORIENTATION); 250 251 RuleAction recentAction = nestedActions.get(0); 252 assertTrue(recentAction instanceof NestedAction); 253 NestedAction recentChoices = (NestedAction) recentAction; 254 List<RuleAction> recentItems = recentChoices.getNestedActions(node); 255 256 assertEquals(1, recentItems.size()); 257 assertEquals("Orientation", recentItems.get(0).getTitle()); 258 259 BaseViewRule.editedProperty("gravity"); 260 recentItems = recentChoices.getNestedActions(node); 261 assertEquals(2, recentItems.size()); 262 assertEquals("Gravity...", recentItems.get(0).getTitle()); 263 assertEquals("Orientation", recentItems.get(1).getTitle()); 264 265 BaseViewRule.editedProperty(ATTR_ORIENTATION); 266 recentItems = recentChoices.getNestedActions(node); 267 assertEquals(2, recentItems.size()); 268 assertEquals("Orientation", recentItems.get(0).getTitle()); 269 assertEquals("Gravity...", recentItems.get(1).getTitle()); 270 271 // Lots of other properties -- flushes out properties that apply to this view 272 for (int i = 0; i < 30; i++) { 273 BaseViewRule.editedProperty("dummy_" + i); 274 } 275 recentItems = recentChoices.getNestedActions(node); 276 assertEquals(0, recentItems.size()); 277 278 BaseViewRule.editedProperty("gravity"); 279 recentItems = recentChoices.getNestedActions(node); 280 assertEquals(1, recentItems.size()); 281 assertEquals("Gravity...", recentItems.get(0).getTitle()); 282 } 283 284 public void testDragInEmptyWithBounds() { 285 dragIntoEmpty(new Rect(0, 0, 100, 80)); 286 } 287 288 public void testDragInEmptyWithoutBounds() { 289 dragIntoEmpty(new Rect(0, 0, 0, 0)); 290 } 291 292 public void testDragInVerticalTop() { 293 dragInto(true, 294 // Bounds of the dragged item 295 new Rect(0, 0, 105, 80), 296 // Drag point 297 new Point(30, -10), 298 // Expected insert location 299 0, 300 // Not dragging one of the existing children 301 -1, 302 // Bounds rectangle 303 "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", 304 305 // Drop zones 306 "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,90,240,90), " 307 + "drawLine(0,190,240,190), drawLine(0,290,240,290), " 308 + "drawLine(0,381,240,381)", 309 310 // Active nearest line 311 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,0,240,0)", 312 313 // Preview of the dropped rectangle 314 "useStyle(DROP_PREVIEW), drawRect(0,-40,105,40)"); 315 316 // Without drag bounds it should be identical except no preview 317 // rectangle 318 dragInto(true, 319 new Rect(0, 0, 0, 0), // Invalid 320 new Point(30, -10), 0, -1, 321 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,0,240,0)"); 322 } 323 324 public void testDragInVerticalBottom() { 325 dragInto(true, 326 // Bounds of the dragged item 327 new Rect(0, 0, 105, 80), 328 // Drag point 329 new Point(30, 500), 330 // Expected insert location 331 4, 332 // Not dragging one of the existing children 333 -1, 334 // Bounds rectangle 335 "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", 336 337 // Drop zones 338 "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,90,240,90), " 339 + "drawLine(0,190,240,190), drawLine(0,290,240,290), drawLine(0,381,240,381), ", 340 341 // Active nearest line 342 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,381,240,381)", 343 344 // Preview of the dropped rectangle 345 "useStyle(DROP_PREVIEW), drawRect(0,381,105,461)"); 346 347 // Check without bounds too 348 dragInto(true, new Rect(0, 0, 105, 80), new Point(30, 500), 4, -1, 349 "useStyle(DROP_PREVIEW), drawRect(0,381,105,461)"); 350 } 351 352 public void testDragInVerticalMiddle() { 353 dragInto(true, 354 // Bounds of the dragged item 355 new Rect(0, 0, 105, 80), 356 // Drag point 357 new Point(0, 170), 358 // Expected insert location 359 2, 360 // Not dragging one of the existing children 361 -1, 362 // Bounds rectangle 363 "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", 364 365 // Drop zones 366 "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,90,240,90), " 367 + "drawLine(0,190,240,190), drawLine(0,290,240,290)", 368 369 // Active nearest line 370 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,190,240,190)", 371 372 // Preview of the dropped rectangle 373 "useStyle(DROP_PREVIEW), drawRect(0,150,105,230)"); 374 375 // Check without bounds too 376 dragInto(true, new Rect(0, 0, 105, 80), new Point(0, 170), 2, -1, 377 "useStyle(DROP_PREVIEW), drawRect(0,150,105,230)"); 378 } 379 380 public void testDragInVerticalMiddleSelfPos() { 381 // Drag the 2nd button, down to the position between 3rd and 4th 382 dragInto(true, 383 // Bounds of the dragged item 384 new Rect(0, 100, 100, 80), 385 // Drag point 386 new Point(0, 250), 387 // Expected insert location 388 2, 389 // Dragging 1st item 390 1, 391 // Bounds rectangle 392 393 "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", 394 395 // Drop zones - these are different because we exclude drop 396 // zones around the 397 // dragged item itself (it doesn't make sense to insert directly 398 // before or after 399 // myself 400 "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,290,240,290), " 401 + "drawLine(0,381,240,381)", 402 403 // Preview line along insert axis 404 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,290,240,290)", 405 406 // Preview of dropped rectangle 407 "useStyle(DROP_PREVIEW), drawRect(0,250,100,330)"); 408 409 // Test dropping on self (no position change): 410 dragInto(true, 411 // Bounds of the dragged item 412 new Rect(0, 100, 100, 80), 413 // Drag point 414 new Point(0, 210), 415 // Expected insert location 416 1, 417 // Dragging from same pos 418 1, 419 // Bounds rectangle 420 "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", 421 422 // Drop zones - these are different because we exclude drop 423 // zones around the 424 // dragged item itself (it doesn't make sense to insert directly 425 // before or after 426 // myself 427 "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,290,240,290), " 428 + "drawLine(0,381,240,381)", 429 430 // No active nearest line when you're over the self pos! 431 432 // Preview of the dropped rectangle 433 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawRect(0,100,100,180)"); 434 } 435 436 public void testDragToLastPosition() { 437 // Drag a button to the last position -- and confirm that the preview rectangle 438 // is now shown midway between the second to last and last positions, but fully 439 // below the drop zone line: 440 dragInto(true, 441 // Bounds of the dragged item 442 new Rect(0, 100, 100, 80), 443 // Drag point 444 new Point(0, 400), 445 // Expected insert location 446 3, 447 // Dragging 1st item 448 1, 449 450 // Bounds rectangle 451 "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", 452 453 // Drop Zones 454 "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,290,240,290), " + 455 "drawLine(0,381,240,381), ", 456 457 // Active Drop Zone 458 "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,381,240,381)", 459 460 // Drop Preview 461 "useStyle(DROP_PREVIEW), drawRect(0,381,100,461)"); 462 } 463 464 public void testFormatFloatValue() throws Exception { 465 assertEquals("1", LinearLayoutRule.formatFloatAttribute(1.0f)); 466 assertEquals("2", LinearLayoutRule.formatFloatAttribute(2.0f)); 467 assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.5f)); 468 assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.50f)); 469 assertEquals("1.51", LinearLayoutRule.formatFloatAttribute(1.51f)); 470 assertEquals("1.51", LinearLayoutRule.formatFloatAttribute(1.514542f)); 471 assertEquals("1.52", LinearLayoutRule.formatFloatAttribute(1.516542f)); 472 assertEquals("-1.51", LinearLayoutRule.formatFloatAttribute(-1.51f)); 473 assertEquals("-1", LinearLayoutRule.formatFloatAttribute(-1f)); 474 } 475 476 public void testFormatFloatValueLocale() throws Exception { 477 // Ensure that the layout float values aren't affected by 478 // locale settings, like using commas instead of of periods 479 Locale originalDefaultLocale = Locale.getDefault(); 480 481 try { 482 Locale.setDefault(Locale.FRENCH); 483 484 // Ensure that this is a locale which uses a comma instead of a period: 485 assertEquals("5,24", String.format("%.2f", 5.236f)); 486 487 // Ensure that the formatFloatAttribute is immune 488 assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.5f)); 489 } finally { 490 Locale.setDefault(originalDefaultLocale); 491 } 492 } 493 494 // Left to test: 495 // Check inserting at last pos with multiple children 496 // Check inserting with no bounds rectangle for dragged element 497 } 498