1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.configuration; 18 19 import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; 20 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; 21 import static com.android.SdkConstants.ATTR_CONTEXT; 22 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 23 import static com.android.SdkConstants.RES_QUALIFIER_SEP; 24 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; 25 import static com.android.SdkConstants.TOOLS_URI; 26 import static com.android.ide.eclipse.adt.AdtUtils.isUiThread; 27 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; 28 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; 29 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER; 30 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_LOCALE; 31 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET; 32 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_THEME; 33 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL; 34 import static com.google.common.base.Objects.equal; 35 36 import com.android.annotations.NonNull; 37 import com.android.annotations.Nullable; 38 import com.android.ide.common.rendering.api.ResourceValue; 39 import com.android.ide.common.rendering.api.StyleResourceValue; 40 import com.android.ide.common.resources.LocaleManager; 41 import com.android.ide.common.resources.ResourceFile; 42 import com.android.ide.common.resources.ResourceFolder; 43 import com.android.ide.common.resources.ResourceRepository; 44 import com.android.ide.common.resources.configuration.DeviceConfigHelper; 45 import com.android.ide.common.resources.configuration.FolderConfiguration; 46 import com.android.ide.common.resources.configuration.LanguageQualifier; 47 import com.android.ide.common.resources.configuration.RegionQualifier; 48 import com.android.ide.common.resources.configuration.ResourceQualifier; 49 import com.android.ide.common.sdk.LoadStatus; 50 import com.android.ide.eclipse.adt.AdtPlugin; 51 import com.android.ide.eclipse.adt.AdtUtils; 52 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 53 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate; 54 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; 55 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 56 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 57 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 58 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; 59 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; 60 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 61 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 62 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 63 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 64 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 65 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 66 import com.android.resources.ResourceType; 67 import com.android.resources.ScreenOrientation; 68 import com.android.sdklib.AndroidVersion; 69 import com.android.sdklib.IAndroidTarget; 70 import com.android.sdklib.devices.Device; 71 import com.android.sdklib.devices.DeviceManager; 72 import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; 73 import com.android.sdklib.devices.State; 74 import com.android.utils.Pair; 75 import com.google.common.base.Objects; 76 import com.google.common.base.Strings; 77 78 import org.eclipse.core.resources.IFile; 79 import org.eclipse.core.resources.IFolder; 80 import org.eclipse.core.resources.IProject; 81 import org.eclipse.jface.resource.ImageDescriptor; 82 import org.eclipse.swt.SWT; 83 import org.eclipse.swt.events.DisposeEvent; 84 import org.eclipse.swt.events.DisposeListener; 85 import org.eclipse.swt.events.SelectionAdapter; 86 import org.eclipse.swt.events.SelectionEvent; 87 import org.eclipse.swt.events.SelectionListener; 88 import org.eclipse.swt.graphics.Image; 89 import org.eclipse.swt.graphics.Point; 90 import org.eclipse.swt.layout.GridData; 91 import org.eclipse.swt.layout.GridLayout; 92 import org.eclipse.swt.widgets.Composite; 93 import org.eclipse.swt.widgets.ToolBar; 94 import org.eclipse.swt.widgets.ToolItem; 95 import org.eclipse.ui.IEditorPart; 96 import org.w3c.dom.Document; 97 import org.w3c.dom.Element; 98 99 import java.util.ArrayList; 100 import java.util.Collection; 101 import java.util.Collections; 102 import java.util.IdentityHashMap; 103 import java.util.List; 104 import java.util.Map; 105 import java.util.SortedSet; 106 107 /** 108 * The {@linkplain ConfigurationChooser} allows the user to pick a 109 * {@link Configuration} by configuring various constraints. 110 */ 111 public class ConfigurationChooser extends Composite 112 implements DevicesChangedListener, DisposeListener { 113 private static final String ICON_SQUARE = "square"; //$NON-NLS-1$ 114 private static final String ICON_LANDSCAPE = "landscape"; //$NON-NLS-1$ 115 private static final String ICON_PORTRAIT = "portrait"; //$NON-NLS-1$ 116 private static final String ICON_LANDSCAPE_FLIP = "flip_landscape";//$NON-NLS-1$ 117 private static final String ICON_PORTRAIT_FLIP = "flip_portrait";//$NON-NLS-1$ 118 private static final String ICON_DISPLAY = "display"; //$NON-NLS-1$ 119 private static final String ICON_THEMES = "themes"; //$NON-NLS-1$ 120 private static final String ICON_ACTIVITY = "activity"; //$NON-NLS-1$ 121 122 /** The configuration state associated with this editor */ 123 private @NonNull Configuration mConfiguration = Configuration.create(this); 124 125 /** Serialized state to use when initializing the configuration after the SDK is loaded */ 126 private String mInitialState; 127 128 /** The client of the configuration editor */ 129 private final ConfigurationClient mClient; 130 131 /** Counter for programmatic UI changes: if greater than 0, we're within a call */ 132 private int mDisableUpdates = 0; 133 134 /** List of available devices */ 135 private List<Device> mDeviceList = Collections.emptyList(); 136 137 /** List of available targets */ 138 private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>(); 139 140 /** List of available themes */ 141 private final List<String> mThemeList = new ArrayList<String>(); 142 143 /** List of available locales */ 144 private final List<Locale > mLocaleList = new ArrayList<Locale>(); 145 146 /** The file being edited */ 147 private IFile mEditedFile; 148 149 /** The {@link ProjectResources} for the edited file's project */ 150 private ProjectResources mResources; 151 152 /** The target of the project of the file being edited. */ 153 private IAndroidTarget mProjectTarget; 154 155 /** Dropdown for configurations */ 156 private ToolItem mConfigCombo; 157 158 /** Dropdown for devices */ 159 private ToolItem mDeviceCombo; 160 161 /** Dropdown for device states */ 162 private ToolItem mOrientationCombo; 163 164 /** Dropdown for themes */ 165 private ToolItem mThemeCombo; 166 167 /** Dropdown for locales */ 168 private ToolItem mLocaleCombo; 169 170 /** Dropdown for activities */ 171 private ToolItem mActivityCombo; 172 173 /** Dropdown for rendering targets */ 174 private ToolItem mTargetCombo; 175 176 /** Whether the SDK has changed since the last model reload; if so we must reload targets */ 177 private boolean mSdkChanged = true; 178 179 /** 180 * Creates a new {@linkplain ConfigurationChooser} and adds it to the 181 * parent. The method also receives custom buttons to set into the 182 * configuration composite. The list is organized as an array of arrays. 183 * Each array represents a group of buttons thematically grouped together. 184 * 185 * @param client the client embedding this configuration chooser 186 * @param parent The parent composite. 187 * @param initialState The initial state (serialized form) to use for the 188 * configuration 189 */ 190 public ConfigurationChooser( 191 @NonNull ConfigurationClient client, 192 Composite parent, 193 @Nullable String initialState) { 194 super(parent, SWT.NONE); 195 mClient = client; 196 197 setVisible(false); // Delayed until the targets are loaded 198 199 mInitialState = initialState; 200 setLayout(new GridLayout(1, false)); 201 202 IconFactory icons = IconFactory.getInstance(); 203 204 // TODO: Consider switching to a CoolBar instead 205 ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); 206 toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); 207 208 mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN ); 209 mConfigCombo.setImage(icons.getIcon("android_file")); //$NON-NLS-1$ 210 mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse"); 211 212 @SuppressWarnings("unused") 213 ToolItem separator2 = new ToolItem(toolBar, SWT.SEPARATOR); 214 215 mDeviceCombo = new ToolItem(toolBar, SWT.DROP_DOWN); 216 mDeviceCombo.setImage(icons.getIcon(ICON_DISPLAY)); 217 218 @SuppressWarnings("unused") 219 ToolItem separator3 = new ToolItem(toolBar, SWT.SEPARATOR); 220 221 mOrientationCombo = new ToolItem(toolBar, SWT.DROP_DOWN); 222 mOrientationCombo.setImage(icons.getIcon(ICON_PORTRAIT)); 223 mOrientationCombo.setToolTipText("Go to next state"); 224 225 @SuppressWarnings("unused") 226 ToolItem separator4 = new ToolItem(toolBar, SWT.SEPARATOR); 227 228 mThemeCombo = new ToolItem(toolBar, SWT.DROP_DOWN); 229 mThemeCombo.setImage(icons.getIcon(ICON_THEMES)); 230 231 @SuppressWarnings("unused") 232 ToolItem separator5 = new ToolItem(toolBar, SWT.SEPARATOR); 233 234 mActivityCombo = new ToolItem(toolBar, SWT.DROP_DOWN); 235 mActivityCombo.setToolTipText("Associated activity or fragment providing context"); 236 // The JDT class icon is lopsided, presumably because they've left room in the 237 // bottom right corner for badges (for static, final etc). Unfortunately, this 238 // means that the icon looks out of place when sitting close to the language globe 239 // icon, the theme icon, etc so that it looks vertically misaligned: 240 //mActivityCombo.setImage(JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS)); 241 // ...so use one that is centered instead: 242 mActivityCombo.setImage(icons.getIcon(ICON_ACTIVITY)); 243 244 @SuppressWarnings("unused") 245 ToolItem separator6 = new ToolItem(toolBar, SWT.SEPARATOR); 246 247 //ToolBar rightToolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); 248 //rightToolBar.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); 249 ToolBar rightToolBar = toolBar; 250 251 mLocaleCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN); 252 mLocaleCombo.setImage(FlagManager.getGlobeIcon()); 253 mLocaleCombo.setToolTipText("Locale to use when rendering layouts in Eclipse"); 254 255 @SuppressWarnings("unused") 256 ToolItem separator7 = new ToolItem(rightToolBar, SWT.SEPARATOR); 257 258 mTargetCombo = new ToolItem(rightToolBar, SWT.DROP_DOWN); 259 mTargetCombo.setImage(AdtPlugin.getAndroidLogo()); 260 mTargetCombo.setToolTipText("Android version to use when rendering layouts in Eclipse"); 261 262 SelectionListener listener = new SelectionAdapter() { 263 @Override 264 public void widgetSelected(SelectionEvent e) { 265 Object source = e.getSource(); 266 267 if (source == mConfigCombo) { 268 ConfigurationMenuListener.show(ConfigurationChooser.this, mConfigCombo); 269 } else if (source == mActivityCombo) { 270 ActivityMenuListener.show(ConfigurationChooser.this, mActivityCombo); 271 } else if (source == mLocaleCombo) { 272 LocaleMenuListener.show(ConfigurationChooser.this, mLocaleCombo); 273 } else if (source == mDeviceCombo) { 274 DeviceMenuListener.show(ConfigurationChooser.this, mDeviceCombo); 275 } else if (source == mTargetCombo) { 276 TargetMenuListener.show(ConfigurationChooser.this, mTargetCombo); 277 } else if (source == mThemeCombo) { 278 ThemeMenuAction.showThemeMenu(ConfigurationChooser.this, mThemeCombo, 279 mThemeList); 280 } else if (source == mOrientationCombo) { 281 if (e.detail == SWT.ARROW) { 282 OrientationMenuAction.showMenu(ConfigurationChooser.this, 283 mOrientationCombo); 284 } else { 285 gotoNextState(); 286 } 287 } 288 } 289 }; 290 mConfigCombo.addSelectionListener(listener); 291 mActivityCombo.addSelectionListener(listener); 292 mLocaleCombo.addSelectionListener(listener); 293 mDeviceCombo.addSelectionListener(listener); 294 mTargetCombo.addSelectionListener(listener); 295 mThemeCombo.addSelectionListener(listener); 296 mOrientationCombo.addSelectionListener(listener); 297 298 addDisposeListener(this); 299 300 initDevices(); 301 initTargets(); 302 } 303 304 /** 305 * Returns the edited file 306 * 307 * @return the file 308 */ 309 @Nullable 310 public IFile getEditedFile() { 311 return mEditedFile; 312 } 313 314 /** 315 * Returns the project of the edited file 316 * 317 * @return the project 318 */ 319 @Nullable 320 public IProject getProject() { 321 if (mEditedFile != null) { 322 return mEditedFile.getProject(); 323 } else { 324 return null; 325 } 326 } 327 328 ConfigurationClient getClient() { 329 return mClient; 330 } 331 332 /** 333 * Returns the project resources for the project being configured by this 334 * chooser 335 * 336 * @return the project resources 337 */ 338 @Nullable 339 public ProjectResources getResources() { 340 return mResources; 341 } 342 343 /** 344 * Returns the full, complete {@link FolderConfiguration} 345 * 346 * @return the full configuration 347 */ 348 public FolderConfiguration getFullConfiguration() { 349 return mConfiguration.getFullConfig(); 350 } 351 352 /** 353 * Returns the project target 354 * 355 * @return the project target 356 */ 357 public IAndroidTarget getProjectTarget() { 358 return mProjectTarget; 359 } 360 361 /** 362 * Returns the configuration being edited by this {@linkplain ConfigurationChooser} 363 * 364 * @return the configuration 365 */ 366 public Configuration getConfiguration() { 367 return mConfiguration; 368 } 369 370 /** 371 * Returns the list of locales 372 * @return a list of {@link ResourceQualifier} pairs 373 */ 374 @NonNull 375 public List<Locale> getLocaleList() { 376 return mLocaleList; 377 } 378 379 /** 380 * Returns the list of available devices 381 * 382 * @return a list of {@link Device} objects 383 */ 384 @NonNull 385 public List<Device> getDeviceList() { 386 return mDeviceList; 387 } 388 389 /** 390 * Returns the list of available render targets 391 * 392 * @return a list of {@link IAndroidTarget} objects 393 */ 394 @NonNull 395 public List<IAndroidTarget> getTargetList() { 396 return mTargetList; 397 } 398 399 // ---- Configuration State Lookup ---- 400 401 /** 402 * Returns the rendering target to be used 403 * 404 * @return the target 405 */ 406 @NonNull 407 public IAndroidTarget getTarget() { 408 IAndroidTarget target = mConfiguration.getTarget(); 409 if (target == null) { 410 target = mProjectTarget; 411 } 412 413 return target; 414 } 415 416 /** 417 * Returns the current device string, or null if no device is selected 418 * 419 * @return the device name, or null 420 */ 421 @Nullable 422 public String getDeviceName() { 423 Device device = mConfiguration.getDevice(); 424 if (device != null) { 425 return device.getName(); 426 } 427 428 return null; 429 } 430 431 /** 432 * Returns the current theme, or null if none has been selected 433 * 434 * @return the theme name, or null 435 */ 436 @Nullable 437 public String getThemeName() { 438 String theme = mConfiguration.getTheme(); 439 if (theme != null) { 440 theme = ResourceHelper.styleToTheme(theme); 441 } 442 443 return theme; 444 } 445 446 /** Move to the next device state, changing the icon if it changes orientation */ 447 private void gotoNextState() { 448 State state = mConfiguration.getDeviceState(); 449 State flipped = mConfiguration.getNextDeviceState(state); 450 if (flipped != state) { 451 selectDeviceState(flipped); 452 onDeviceConfigChange(); 453 } 454 } 455 456 // ---- Implements DisposeListener ---- 457 458 @Override 459 public void widgetDisposed(DisposeEvent e) { 460 dispose(); 461 } 462 463 @Override 464 public void dispose() { 465 if (!isDisposed()) { 466 super.dispose(); 467 468 final Sdk sdk = Sdk.getCurrent(); 469 if (sdk != null) { 470 DeviceManager manager = sdk.getDeviceManager(); 471 manager.unregisterListener(this); 472 } 473 } 474 } 475 476 // ---- Init and reset/reload methods ---- 477 478 /** 479 * Sets the reference to the file being edited. 480 * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is 481 * loaded (or reloaded as the SDK/target changes). 482 * 483 * @param file the file being opened 484 * 485 * @see #onXmlModelLoaded() 486 * @see #replaceFile(IFile) 487 * @see #changeFileOnNewConfig(IFile) 488 */ 489 public void setFile(IFile file) { 490 mEditedFile = file; 491 ensureInitialized(); 492 } 493 494 /** 495 * Replaces the UI with a given file configuration. This is meant to answer the user 496 * explicitly opening a different version of the same layout from the Package Explorer. 497 * <p/>This attempts to keep the current config, but may change it if it's not compatible or 498 * not the best match 499 * @param file the file being opened. 500 */ 501 public void replaceFile(IFile file) { 502 // if there is no previous selection, revert to default mode. 503 if (mConfiguration.getDevice() == null) { 504 setFile(file); // onTargetChanged will be called later. 505 return; 506 } 507 508 setFile(file); 509 IProject project = mEditedFile.getProject(); 510 mResources = ResourceManager.getInstance().getProjectResources(project); 511 512 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); 513 mConfiguration.setEditedConfig(resFolder.getConfiguration()); 514 515 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 516 // new values in the widgets. 517 518 try { 519 // only attempt to do anything if the SDK and targets are loaded. 520 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 521 522 if (sdkStatus == LoadStatus.LOADED) { 523 setVisible(true); 524 525 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, 526 null /*project*/); 527 528 if (targetStatus == LoadStatus.LOADED) { 529 530 // update the current config selection to make sure it's 531 // compatible with the new file 532 ConfigurationMatcher matcher = new ConfigurationMatcher(this); 533 matcher.adaptConfigSelection(true /*needBestMatch*/); 534 mConfiguration.syncFolderConfig(); 535 536 // update the string showing the config value 537 selectConfiguration(mConfiguration.getEditedConfig()); 538 updateActivity(); 539 } 540 } else if (sdkStatus == LoadStatus.FAILED) { 541 setVisible(true); 542 } 543 } finally { 544 mDisableUpdates--; 545 } 546 } 547 548 /** 549 * Updates the UI with a new file that was opened in response to a config change. 550 * @param file the file being opened. 551 * 552 * @see #replaceFile(IFile) 553 */ 554 public void changeFileOnNewConfig(IFile file) { 555 setFile(file); 556 IProject project = mEditedFile.getProject(); 557 mResources = ResourceManager.getInstance().getProjectResources(project); 558 559 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); 560 FolderConfiguration config = resFolder.getConfiguration(); 561 mConfiguration.setEditedConfig(config); 562 563 // All that's needed is to update the string showing the config value 564 // (since the config combo settings chosen by the user). 565 selectConfiguration(config); 566 } 567 568 /** 569 * Resets the configuration chooser to reflect the given file configuration. This is 570 * intended to be used by the "Show Included In" functionality where the user has 571 * picked a non-default configuration (such as a particular landscape layout) and the 572 * configuration chooser must be switched to a landscape layout. This method will 573 * trigger a model change. 574 * <p> 575 * This will NOT trigger a redraw event! 576 * <p> 577 * FIXME: We are currently setting the configuration file to be the configuration for 578 * the "outer" (the including) file, rather than the inner file, which is the file the 579 * user is actually editing. We need to refine this, possibly with a way for the user 580 * to choose which configuration they are editing. And in particular, we should be 581 * filtering the configuration chooser to only show options in the outer configuration 582 * that are compatible with the inner included file. 583 * 584 * @param file the file to be configured 585 */ 586 public void resetConfigFor(IFile file) { 587 setFile(file); 588 589 IFolder parent = (IFolder) mEditedFile.getParent(); 590 ResourceFolder resFolder = mResources.getResourceFolder(parent); 591 if (resFolder != null) { 592 mConfiguration.setEditedConfig(resFolder.getConfiguration()); 593 } else { 594 FolderConfiguration config = FolderConfiguration.getConfig( 595 parent.getName().split(RES_QUALIFIER_SEP)); 596 if (config != null) { 597 mConfiguration.setEditedConfig(config); 598 } else { 599 mConfiguration.setEditedConfig(new FolderConfiguration()); 600 } 601 } 602 603 onXmlModelLoaded(); 604 } 605 606 607 /** 608 * Sets the current configuration to match the given folder configuration, 609 * the given theme name, the given device and device state. 610 * 611 * @param configuration new folder configuration to use 612 */ 613 public void setConfiguration(@NonNull Configuration configuration) { 614 if (mClient != null) { 615 mClient.aboutToChange(MASK_ALL); 616 } 617 618 Configuration oldConfiguration = mConfiguration; 619 mConfiguration = configuration; 620 mConfiguration.setChooser(this); 621 622 selectTheme(configuration.getTheme()); 623 selectLocale(configuration.getLocale()); 624 selectDevice(configuration.getDevice()); 625 selectDeviceState(configuration.getDeviceState()); 626 selectTarget(configuration.getTarget()); 627 selectActivity(configuration.getActivity()); 628 629 // This may be a second refresh after triggered by theme above 630 if (mClient != null) { 631 LayoutCanvas canvas = mClient.getCanvas(); 632 if (canvas != null) { 633 assert mConfiguration != oldConfiguration; 634 canvas.getPreviewManager().updateChooserConfig(oldConfiguration, mConfiguration); 635 } 636 637 boolean accepted = mClient.changed(MASK_ALL); 638 if (!accepted) { 639 configuration = oldConfiguration; 640 selectTheme(configuration.getTheme()); 641 selectLocale(configuration.getLocale()); 642 selectDevice(configuration.getDevice()); 643 selectDeviceState(configuration.getDeviceState()); 644 selectTarget(configuration.getTarget()); 645 selectActivity(configuration.getActivity()); 646 if (canvas != null && mConfiguration != oldConfiguration) { 647 canvas.getPreviewManager().updateChooserConfig(mConfiguration, 648 oldConfiguration); 649 } 650 return; 651 } else { 652 int changed = 0; 653 if (!equal(oldConfiguration.getTheme(), mConfiguration.getTheme())) { 654 changed |= CFG_THEME; 655 } 656 if (!equal(oldConfiguration.getDevice(), mConfiguration.getDevice())) { 657 changed |= CFG_DEVICE | CFG_DEVICE_STATE; 658 } 659 if (changed != 0) { 660 syncToVariations(changed, mEditedFile, mConfiguration, false, true); 661 } 662 } 663 } 664 665 saveConstraints(); 666 } 667 668 /** 669 * Responds to the event that the basic SDK information finished loading. 670 * @param target the possibly new target object associated with the file being edited (in case 671 * the SDK path was changed). 672 */ 673 public void onSdkLoaded(IAndroidTarget target) { 674 // a change to the SDK means that we need to check for new/removed devices. 675 mSdkChanged = true; 676 677 // store the new target. 678 mProjectTarget = target; 679 680 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 681 // new values in the widgets. 682 try { 683 updateDevices(); 684 updateTargets(); 685 ensureInitialized(); 686 } finally { 687 mDisableUpdates--; 688 } 689 } 690 691 /** 692 * Responds to the XML model being loaded, either the first time or when the 693 * Target/SDK changes. 694 * <p> 695 * This initializes the UI, either with the first compatible configuration 696 * found, or it will attempt to restore a configuration if one is found to 697 * have been saved in the file persistent storage. 698 * <p> 699 * If the SDK or target are not loaded, nothing will happen (but the method 700 * must be called back when they are.) 701 * <p> 702 * The method automatically handles being called the first time after editor 703 * creation, or being called after during SDK/Target changes (as long as 704 * {@link #onSdkLoaded(IAndroidTarget)} is properly called). 705 * 706 * @return the target data for the rendering target used to render the 707 * layout 708 * 709 * @see #saveConstraints() 710 * @see #onSdkLoaded(IAndroidTarget) 711 */ 712 public AndroidTargetData onXmlModelLoaded() { 713 AndroidTargetData targetData = null; 714 715 // only attempt to do anything if the SDK and targets are loaded. 716 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 717 if (sdkStatus == LoadStatus.LOADED) { 718 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 719 720 try { 721 // init the devices if needed (new SDK or first time going through here) 722 if (mSdkChanged) { 723 updateDevices(); 724 updateTargets(); 725 ensureInitialized(); 726 mSdkChanged = false; 727 } 728 729 IProject project = mEditedFile.getProject(); 730 731 Sdk currentSdk = Sdk.getCurrent(); 732 if (currentSdk != null) { 733 mProjectTarget = currentSdk.getTarget(project); 734 } 735 736 LoadStatus targetStatus = LoadStatus.FAILED; 737 if (mProjectTarget != null) { 738 targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null); 739 updateTargets(); 740 ensureInitialized(); 741 } 742 743 if (targetStatus == LoadStatus.LOADED) { 744 setVisible(true); 745 if (mResources == null) { 746 mResources = ResourceManager.getInstance().getProjectResources(project); 747 } 748 if (mConfiguration.getEditedConfig() == null) { 749 IFolder parent = (IFolder) mEditedFile.getParent(); 750 ResourceFolder resFolder = mResources.getResourceFolder(parent); 751 if (resFolder != null) { 752 mConfiguration.setEditedConfig(resFolder.getConfiguration()); 753 } else { 754 FolderConfiguration config = FolderConfiguration.getConfig( 755 parent.getName().split(RES_QUALIFIER_SEP)); 756 if (config != null) { 757 mConfiguration.setEditedConfig(config); 758 } else { 759 mConfiguration.setEditedConfig(new FolderConfiguration()); 760 } 761 } 762 } 763 764 targetData = Sdk.getCurrent().getTargetData(mProjectTarget); 765 766 // get the file stored state 767 ensureInitialized(); 768 boolean loadedConfigData = mConfiguration.getDevice() != null && 769 mConfiguration.getDeviceState() != null; 770 771 // Load locale list. This must be run after we initialize the 772 // configuration above, since it attempts to sync the UI with 773 // the value loaded into the configuration. 774 updateLocales(); 775 776 // If the current state was loaded from the persistent storage, we update the 777 // UI with it and then try to adapt it (which will handle incompatible 778 // configuration). 779 // Otherwise, just look for the first compatible configuration. 780 ConfigurationMatcher matcher = new ConfigurationMatcher(this); 781 if (loadedConfigData) { 782 // first make sure we have the config to adapt 783 selectDevice(mConfiguration.getDevice()); 784 selectDeviceState(mConfiguration.getDeviceState()); 785 mConfiguration.syncFolderConfig(); 786 787 matcher.adaptConfigSelection(false); 788 789 IAndroidTarget target = mConfiguration.getTarget(); 790 selectTarget(target); 791 targetData = Sdk.getCurrent().getTargetData(target); 792 } else { 793 matcher.findAndSetCompatibleConfig(false); 794 795 // Default to modern layout lib 796 IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(this); 797 if (target != null) { 798 targetData = Sdk.getCurrent().getTargetData(target); 799 selectTarget(target); 800 mConfiguration.setTarget(target, true); 801 } 802 } 803 804 // Update activity: This is done before updateThemes() since 805 // the themes selection can depend on the currently selected activity 806 // (e.g. when there are manifest registrations for the theme to use 807 // for a given activity) 808 updateActivity(); 809 810 // Update themes. This is done after updating the devices above, 811 // since we want to look at the chosen device size to decide 812 // what the default theme (for example, with Honeycomb we choose 813 // Holo as the default theme but only if the screen size is XLARGE 814 // (and of course only if the manifest does not specify another 815 // default theme). 816 updateThemes(); 817 818 // update the string showing the config value 819 selectConfiguration(mConfiguration.getEditedConfig()); 820 821 // compute the final current config 822 mConfiguration.syncFolderConfig(); 823 } else if (targetStatus == LoadStatus.FAILED) { 824 setVisible(true); 825 } 826 } finally { 827 mDisableUpdates--; 828 } 829 } 830 831 return targetData; 832 } 833 834 /** 835 * This is a temporary workaround for a infrequently happening bug; apparently 836 * there are cases where the configuration chooser isn't shown 837 */ 838 public void ensureVisible() { 839 if (!isVisible()) { 840 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 841 if (sdkStatus == LoadStatus.LOADED) { 842 onXmlModelLoaded(); 843 } 844 } 845 } 846 847 /** 848 * An alternate layout for this layout has been created. This means that the 849 * current layout may no longer be a best fit. However, since we support multiple 850 * layouts being open at the same time, we need to adjust the current configuration 851 * back to something where this layout <b>is</b> a best match. 852 */ 853 public void onAlternateLayoutCreated() { 854 IFile best = ConfigurationMatcher.getBestFileMatch(this); 855 if (best != null && !best.equals(mEditedFile)) { 856 ConfigurationMatcher matcher = new ConfigurationMatcher(this); 857 matcher.adaptConfigSelection(true /*needBestMatch*/); 858 mConfiguration.syncFolderConfig(); 859 if (mClient != null) { 860 mClient.changed(MASK_ALL); 861 } 862 } 863 } 864 865 /** 866 * Loads the list of {@link Device}s and inits the UI with it. 867 */ 868 private void initDevices() { 869 final Sdk sdk = Sdk.getCurrent(); 870 if (sdk != null) { 871 DeviceManager manager = sdk.getDeviceManager(); 872 // This method can be called more than once, so avoid duplicate entries 873 manager.unregisterListener(this); 874 manager.registerListener(this); 875 mDeviceList = manager.getDevices(DeviceManager.ALL_DEVICES); 876 } else { 877 mDeviceList = new ArrayList<Device>(); 878 } 879 } 880 881 /** 882 * Loads the list of {@link IAndroidTarget} and inits the UI with it. 883 */ 884 private boolean initTargets() { 885 mTargetList.clear(); 886 887 Sdk currentSdk = Sdk.getCurrent(); 888 if (currentSdk != null) { 889 IAndroidTarget[] targets = currentSdk.getTargets(); 890 for (int i = 0 ; i < targets.length; i++) { 891 if (targets[i].hasRenderingLibrary()) { 892 mTargetList.add(targets[i]); 893 } 894 } 895 896 return true; 897 } 898 899 return false; 900 } 901 902 /** Ensures that the configuration has been initialized */ 903 public void ensureInitialized() { 904 if (mConfiguration.getDevice() == null && mEditedFile != null) { 905 String data = ConfigurationDescription.getDescription(mEditedFile); 906 if (mInitialState != null) { 907 data = mInitialState; 908 mInitialState = null; 909 } 910 if (data != null) { 911 mConfiguration.initialize(data); 912 mConfiguration.syncFolderConfig(); 913 } 914 } 915 } 916 917 private void updateDevices() { 918 if (mDeviceList.size() == 0) { 919 initDevices(); 920 } 921 } 922 923 private void updateTargets() { 924 if (mTargetList.size() == 0) { 925 if (!initTargets()) { 926 return; 927 } 928 } 929 930 IAndroidTarget renderingTarget = mConfiguration.getTarget(); 931 932 IAndroidTarget match = null; 933 for (IAndroidTarget target : mTargetList) { 934 if (renderingTarget != null) { 935 // use equals because the rendering could be from a previous SDK, so 936 // it may not be the same instance. 937 if (renderingTarget.equals(target)) { 938 match = target; 939 } 940 } else if (mProjectTarget == target) { 941 match = target; 942 } 943 944 } 945 946 if (match == null) { 947 // the rendering target is the same as the project. 948 renderingTarget = mProjectTarget; 949 } else { 950 // set the rendering target to the new object. 951 renderingTarget = match; 952 } 953 954 mConfiguration.setTarget(renderingTarget, true); 955 selectTarget(renderingTarget); 956 } 957 958 /** Update the toolbar whenever a label has changed, to not only 959 * cause the layout in the current toolbar to update, but to possibly 960 * wrap the toolbars and update the layout of the surrounding area. 961 */ 962 private void resizeToolBar() { 963 Point size = getSize(); 964 Point newSize = computeSize(size.x, SWT.DEFAULT, true); 965 setSize(newSize); 966 Composite parent = getParent(); 967 parent.layout(); 968 parent.redraw(); 969 } 970 971 972 Image getOrientationIcon(ScreenOrientation orientation, boolean flip) { 973 IconFactory icons = IconFactory.getInstance(); 974 switch (orientation) { 975 case LANDSCAPE: 976 return icons.getIcon(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE); 977 case SQUARE: 978 return icons.getIcon(ICON_SQUARE); 979 case PORTRAIT: 980 default: 981 return icons.getIcon(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT); 982 } 983 } 984 985 ImageDescriptor getOrientationImage(ScreenOrientation orientation, boolean flip) { 986 IconFactory icons = IconFactory.getInstance(); 987 switch (orientation) { 988 case LANDSCAPE: 989 return icons.getImageDescriptor(flip ? ICON_LANDSCAPE_FLIP : ICON_LANDSCAPE); 990 case SQUARE: 991 return icons.getImageDescriptor(ICON_SQUARE); 992 case PORTRAIT: 993 default: 994 return icons.getImageDescriptor(flip ? ICON_PORTRAIT_FLIP : ICON_PORTRAIT); 995 } 996 } 997 998 @NonNull 999 ScreenOrientation getOrientation(State state) { 1000 FolderConfiguration config = DeviceConfigHelper.getFolderConfig(state); 1001 ScreenOrientation orientation = null; 1002 if (config != null && config.getScreenOrientationQualifier() != null) { 1003 orientation = config.getScreenOrientationQualifier().getValue(); 1004 } 1005 1006 if (orientation == null) { 1007 orientation = ScreenOrientation.PORTRAIT; 1008 } 1009 1010 return orientation; 1011 } 1012 1013 /** 1014 * Stores the current config selection into the edited file such that we can 1015 * bring it back the next time this layout is opened. 1016 */ 1017 public void saveConstraints() { 1018 String description = mConfiguration.toPersistentString(); 1019 if (description != null && !description.isEmpty()) { 1020 ConfigurationDescription.setDescription(mEditedFile, description); 1021 } 1022 } 1023 1024 // ---- Setting the current UI state ---- 1025 1026 void selectDeviceState(@Nullable State state) { 1027 assert isUiThread(); 1028 try { 1029 mDisableUpdates++; 1030 mOrientationCombo.setData(state); 1031 1032 State nextState = mConfiguration.getNextDeviceState(state); 1033 mOrientationCombo.setImage(getOrientationIcon(getOrientation(state), 1034 nextState != state)); 1035 } finally { 1036 mDisableUpdates--; 1037 } 1038 } 1039 1040 void selectTarget(IAndroidTarget target) { 1041 assert isUiThread(); 1042 try { 1043 mDisableUpdates++; 1044 mTargetCombo.setData(target); 1045 String label = getRenderingTargetLabel(target, true); 1046 mTargetCombo.setText(label); 1047 resizeToolBar(); 1048 } finally { 1049 mDisableUpdates--; 1050 } 1051 } 1052 1053 /** 1054 * Selects a given {@link Device} in the device combo, if it is found. 1055 * @param device the device to select 1056 * @return true if the device was found. 1057 */ 1058 boolean selectDevice(@Nullable Device device) { 1059 assert isUiThread(); 1060 try { 1061 mDisableUpdates++; 1062 mDeviceCombo.setData(device); 1063 if (device != null) { 1064 mDeviceCombo.setText(getDeviceLabel(device, true)); 1065 } else { 1066 mDeviceCombo.setText("Device"); 1067 } 1068 resizeToolBar(); 1069 } finally { 1070 mDisableUpdates--; 1071 } 1072 1073 return false; 1074 } 1075 1076 void selectActivity(@Nullable String fqcn) { 1077 assert isUiThread(); 1078 try { 1079 mDisableUpdates++; 1080 if (fqcn != null) { 1081 mActivityCombo.setData(fqcn); 1082 String label = getActivityLabel(fqcn, true); 1083 mActivityCombo.setText(label); 1084 } else { 1085 mActivityCombo.setText("(Select)"); 1086 } 1087 resizeToolBar(); 1088 } finally { 1089 mDisableUpdates--; 1090 } 1091 } 1092 1093 void selectTheme(@Nullable String theme) { 1094 assert isUiThread(); 1095 try { 1096 mDisableUpdates++; 1097 assert theme == null || theme.startsWith(STYLE_RESOURCE_PREFIX) 1098 || theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : theme; 1099 mThemeCombo.setData(theme); 1100 if (theme != null) { 1101 mThemeCombo.setText(getThemeLabel(theme, true)); 1102 } else { 1103 // FIXME eclipse claims this is dead code. 1104 mThemeCombo.setText("(Set Theme)"); 1105 } 1106 resizeToolBar(); 1107 } finally { 1108 mDisableUpdates--; 1109 } 1110 } 1111 1112 void selectLocale(@Nullable Locale locale) { 1113 assert isUiThread(); 1114 try { 1115 mDisableUpdates++; 1116 mLocaleCombo.setData(locale); 1117 String label = Strings.nullToEmpty(getLocaleLabel(this, locale, true)); 1118 mLocaleCombo.setText(label); 1119 1120 Image image = getFlagImage(locale); 1121 mLocaleCombo.setImage(image); 1122 1123 resizeToolBar(); 1124 } finally { 1125 mDisableUpdates--; 1126 } 1127 } 1128 1129 @NonNull 1130 Image getFlagImage(@Nullable Locale locale) { 1131 if (locale != null) { 1132 return locale.getFlagImage(); 1133 } 1134 1135 return FlagManager.getGlobeIcon(); 1136 } 1137 1138 private void selectConfiguration(FolderConfiguration fileConfig) { 1139 /* For now, don't show any text in the configuration combo, use just an 1140 icon. This has the advantage that the configuration contents don't 1141 shift around, so you can for example click back and forth between 1142 portrait and landscape without the icon moving under the mouse. 1143 If this works well, remove this whole method post ADT 21. 1144 assert isUiThread(); 1145 try { 1146 String current = mEditedFile.getParent().getName(); 1147 if (current.equals(FD_RES_LAYOUT)) { 1148 current = "default"; 1149 } 1150 1151 // Pretty things up a bit 1152 //if (current == null || current.equals("default")) { 1153 // current = "Default Configuration"; 1154 //} 1155 mConfigCombo.setText(current); 1156 resizeToolBar(); 1157 } finally { 1158 mDisableUpdates--; 1159 } 1160 */ 1161 } 1162 1163 /** 1164 * Finds a locale matching the config from a file. 1165 * 1166 * @param language the language qualifier or null if none is set. 1167 * @param region the region qualifier or null if none is set. 1168 * @return true if there was a change in the combobox as a result of 1169 * applying the locale 1170 */ 1171 private boolean setLocale(@Nullable Locale locale) { 1172 boolean changed = !Objects.equal(mConfiguration.getLocale(), locale); 1173 selectLocale(locale); 1174 1175 return changed; 1176 } 1177 1178 // ---- Creating UI labels ---- 1179 1180 /** 1181 * Returns a suitable label to use to display the given activity 1182 * 1183 * @param fqcn the activity class to look up a label for 1184 * @param brief if true, generate a brief label (suitable for a toolbar 1185 * button), otherwise a fuller name (suitable for a menu item) 1186 * @return the label 1187 */ 1188 public static String getActivityLabel(String fqcn, boolean brief) { 1189 if (brief) { 1190 String label = fqcn; 1191 int packageIndex = label.lastIndexOf('.'); 1192 if (packageIndex != -1) { 1193 label = label.substring(packageIndex + 1); 1194 } 1195 int innerClass = label.lastIndexOf('$'); 1196 if (innerClass != -1) { 1197 label = label.substring(innerClass + 1); 1198 } 1199 1200 // Also strip out the "Activity" or "Fragment" common suffix 1201 // if this is a long name 1202 if (label.endsWith("Activity") && label.length() > 8 + 12) { // 12 chars + 8 in suffix 1203 label = label.substring(0, label.length() - 8); 1204 } else if (label.endsWith("Fragment") && label.length() > 8 + 12) { 1205 label = label.substring(0, label.length() - 8); 1206 } 1207 1208 return label; 1209 } 1210 1211 return fqcn; 1212 } 1213 1214 /** 1215 * Returns a suitable label to use to display the given theme 1216 * 1217 * @param theme the theme to produce a label for 1218 * @param brief if true, generate a brief label (suitable for a toolbar 1219 * button), otherwise a fuller name (suitable for a menu item) 1220 * @return the label 1221 */ 1222 public static String getThemeLabel(String theme, boolean brief) { 1223 theme = ResourceHelper.styleToTheme(theme); 1224 1225 if (brief) { 1226 int index = theme.lastIndexOf('.'); 1227 if (index < theme.length() - 1) { 1228 return theme.substring(index + 1); 1229 } 1230 } 1231 return theme; 1232 } 1233 1234 /** 1235 * Returns a suitable label to use to display the given rendering target 1236 * 1237 * @param target the target to produce a label for 1238 * @param brief if true, generate a brief label (suitable for a toolbar 1239 * button), otherwise a fuller name (suitable for a menu item) 1240 * @return the label 1241 */ 1242 public static String getRenderingTargetLabel(IAndroidTarget target, boolean brief) { 1243 if (target == null) { 1244 return "<null>"; 1245 } 1246 1247 AndroidVersion version = target.getVersion(); 1248 1249 if (brief) { 1250 if (target.isPlatform()) { 1251 return Integer.toString(version.getApiLevel()); 1252 } else { 1253 return target.getName() + ':' + Integer.toString(version.getApiLevel()); 1254 } 1255 } 1256 1257 String label = String.format("API %1$d: %2$s", 1258 version.getApiLevel(), 1259 target.getShortClasspathName()); 1260 1261 return label; 1262 } 1263 1264 /** 1265 * Returns a suitable label to use to display the given device 1266 * 1267 * @param device the device to produce a label for 1268 * @param brief if true, generate a brief label (suitable for a toolbar 1269 * button), otherwise a fuller name (suitable for a menu item) 1270 * @return the label 1271 */ 1272 public static String getDeviceLabel(@Nullable Device device, boolean brief) { 1273 if (device == null) { 1274 return ""; 1275 } 1276 String name = device.getName(); 1277 1278 if (brief) { 1279 // Produce a really brief summary of the device name, suitable for 1280 // use in the narrow space available in the toolbar for example 1281 int nexus = name.indexOf("Nexus"); //$NON-NLS-1$ 1282 if (nexus != -1) { 1283 int begin = name.indexOf('('); 1284 if (begin != -1) { 1285 begin++; 1286 int end = name.indexOf(')', begin); 1287 if (end != -1) { 1288 return name.substring(begin, end).trim(); 1289 } 1290 } 1291 } 1292 } 1293 1294 return name; 1295 } 1296 1297 /** 1298 * Returns a suitable label to use to display the given locale 1299 * 1300 * @param chooser the chooser, if known 1301 * @param locale the locale to look up a label for 1302 * @param brief if true, generate a brief label (suitable for a toolbar 1303 * button), otherwise a fuller name (suitable for a menu item) 1304 * @return the label 1305 */ 1306 @Nullable 1307 public static String getLocaleLabel( 1308 @Nullable ConfigurationChooser chooser, 1309 @Nullable Locale locale, 1310 boolean brief) { 1311 if (locale == null) { 1312 return null; 1313 } 1314 1315 if (!locale.hasLanguage()) { 1316 if (brief) { 1317 // Just use the icon 1318 return ""; 1319 } 1320 1321 boolean hasLocale = false; 1322 ResourceRepository projectRes = chooser != null ? chooser.mClient.getProjectResources() 1323 : null; 1324 if (projectRes != null) { 1325 hasLocale = projectRes.getLanguages().size() > 0; 1326 } 1327 1328 if (hasLocale) { 1329 return "Other"; 1330 } else { 1331 return "Any"; 1332 } 1333 } 1334 1335 String languageCode = locale.language.getValue(); 1336 String languageName = LocaleManager.getLanguageName(languageCode); 1337 1338 if (!locale.hasRegion()) { 1339 // TODO: Make the region string use "Other" instead of "Any" if 1340 // there is more than one region for a given language 1341 //if (regions.size() > 0) { 1342 // return String.format("%1$s / Other", language); 1343 //} else { 1344 // return String.format("%1$s / Any", language); 1345 //} 1346 if (!brief && languageName != null) { 1347 return String.format("%1$s (%2$s)", languageName, languageCode); 1348 } else { 1349 return languageCode; 1350 } 1351 } else { 1352 String regionCode = locale.region.getValue(); 1353 if (!brief && languageName != null) { 1354 String regionName = LocaleManager.getRegionName(regionCode); 1355 if (regionName != null) { 1356 return String.format("%1$s (%2$s) in %3$s (%4$s)", languageName, languageCode, 1357 regionName, regionCode); 1358 } 1359 return String.format("%1$s (%2$s) in %3$s", languageName, languageCode, 1360 regionCode); 1361 } 1362 return String.format("%1$s / %2$s", languageCode, regionCode); 1363 } 1364 } 1365 1366 // ---- Implements DevicesChangedListener ---- 1367 1368 @Override 1369 public void onDevicesChanged() { 1370 final Sdk sdk = Sdk.getCurrent(); 1371 if (sdk != null) { 1372 mDeviceList = sdk.getDeviceManager().getDevices(DeviceManager.ALL_DEVICES); 1373 } else { 1374 mDeviceList = new ArrayList<Device>(); 1375 } 1376 } 1377 1378 // ---- Reacting to UI changes ---- 1379 1380 /** 1381 * Called when the selection of the device combo changes. 1382 */ 1383 void onDeviceChange() { 1384 // because changing the content of a combo triggers a change event, respect the 1385 // mDisableUpdates flag 1386 if (mDisableUpdates > 0) { 1387 return; 1388 } 1389 1390 // Attempt to preserve the device state 1391 String stateName = null; 1392 Device prevDevice = mConfiguration.getDevice(); 1393 State prevState = mConfiguration.getDeviceState(); 1394 Device device = (Device) mDeviceCombo.getData(); 1395 if (prevDevice != null && prevState != null && device != null) { 1396 // get the previous config, so that we can look for a close match 1397 FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState); 1398 if (oldConfig != null) { 1399 stateName = ConfigurationMatcher.getClosestMatch(oldConfig, device.getAllStates()); 1400 } 1401 } 1402 mConfiguration.setDevice(device, true); 1403 State newState = Configuration.getState(device, stateName); 1404 mConfiguration.setDeviceState(newState, true); 1405 selectDeviceState(newState); 1406 mConfiguration.syncFolderConfig(); 1407 1408 // Notify 1409 IFile file = mEditedFile; 1410 boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE); 1411 if (!accepted) { 1412 mConfiguration.setDevice(prevDevice, true); 1413 mConfiguration.setDeviceState(prevState, true); 1414 mConfiguration.syncFolderConfig(); 1415 selectDevice(prevDevice); 1416 selectDeviceState(prevState); 1417 return; 1418 } else { 1419 syncToVariations(CFG_DEVICE | CFG_DEVICE_STATE, file, mConfiguration, false, true); 1420 } 1421 1422 saveConstraints(); 1423 } 1424 1425 /** 1426 * Synchronizes changes to the given attributes (indicated by the mask 1427 * referencing the {@code CFG_} configuration attribute bit flags in 1428 * {@link Configuration} to the layout variations of the given updated file. 1429 * 1430 * @param flags the attributes which were updated 1431 * @param updatedFile the file which was updated 1432 * @param base the base configuration to base the chooser off of 1433 * @param includeSelf whether the updated file itself should be updated 1434 * @param async whether the updates should be performed asynchronously 1435 */ 1436 public void syncToVariations( 1437 final int flags, 1438 final @NonNull IFile updatedFile, 1439 final @NonNull Configuration base, 1440 final boolean includeSelf, 1441 boolean async) { 1442 if (async) { 1443 getDisplay().asyncExec(new Runnable() { 1444 @Override 1445 public void run() { 1446 doSyncToVariations(flags, updatedFile, includeSelf, base); 1447 } 1448 }); 1449 } else { 1450 doSyncToVariations(flags, updatedFile, includeSelf, base); 1451 } 1452 } 1453 1454 private void doSyncToVariations(int flags, IFile updatedFile, boolean includeSelf, 1455 Configuration base) { 1456 // Synchronize the given changes to other configurations as well 1457 List<IFile> files = AdtUtils.getResourceVariations(updatedFile, includeSelf); 1458 for (IFile file : files) { 1459 Configuration configuration = Configuration.create(base, file); 1460 configuration.setTheme(base.getTheme()); 1461 configuration.setActivity(base.getActivity()); 1462 Collection<IEditorPart> editors = AdtUtils.findEditorsFor(file, false); 1463 boolean found = false; 1464 for (IEditorPart editor : editors) { 1465 if (editor instanceof CommonXmlEditor) { 1466 CommonXmlDelegate delegate = ((CommonXmlEditor) editor).getDelegate(); 1467 if (delegate instanceof LayoutEditorDelegate) { 1468 editor = ((LayoutEditorDelegate) delegate).getGraphicalEditor(); 1469 } 1470 } 1471 if (editor instanceof GraphicalEditorPart) { 1472 ConfigurationChooser chooser = 1473 ((GraphicalEditorPart) editor).getConfigurationChooser(); 1474 chooser.setConfiguration(configuration); 1475 found = true; 1476 } 1477 } 1478 if (!found) { 1479 // Just update the file persistence 1480 String description = configuration.toPersistentString(); 1481 ConfigurationDescription.setDescription(file, description); 1482 } 1483 } 1484 } 1485 1486 /** 1487 * Called when the device config selection changes. 1488 */ 1489 void onDeviceConfigChange() { 1490 // because changing the content of a combo triggers a change event, respect the 1491 // mDisableUpdates flag 1492 if (mDisableUpdates > 0) { 1493 return; 1494 } 1495 1496 State prev = mConfiguration.getDeviceState(); 1497 State state = (State) mOrientationCombo.getData(); 1498 mConfiguration.setDeviceState(state, false); 1499 1500 if (mClient != null) { 1501 boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE); 1502 if (!accepted) { 1503 mConfiguration.setDeviceState(prev, false); 1504 selectDeviceState(prev); 1505 return; 1506 } 1507 } 1508 1509 saveConstraints(); 1510 } 1511 1512 /** 1513 * Call back for language combo selection 1514 */ 1515 void onLocaleChange() { 1516 // because mLocaleList triggers onLocaleChange at each modification, the filling 1517 // of the combo with data will trigger notifications, and we don't want that. 1518 if (mDisableUpdates > 0) { 1519 return; 1520 } 1521 1522 Locale prev = mConfiguration.getLocale(); 1523 Locale locale = (Locale) mLocaleCombo.getData(); 1524 if (locale == null) { 1525 locale = Locale.ANY; 1526 } 1527 mConfiguration.setLocale(locale, false); 1528 1529 if (mClient != null) { 1530 boolean accepted = mClient.changed(CFG_LOCALE); 1531 if (!accepted) { 1532 mConfiguration.setLocale(prev, false); 1533 selectLocale(prev); 1534 } 1535 } 1536 1537 // Store locale project-wide setting 1538 mConfiguration.saveRenderState(); 1539 } 1540 1541 1542 void onThemeChange() { 1543 if (mDisableUpdates > 0) { 1544 return; 1545 } 1546 1547 String prev = mConfiguration.getTheme(); 1548 mConfiguration.setTheme((String) mThemeCombo.getData()); 1549 1550 if (mClient != null) { 1551 boolean accepted = mClient.changed(CFG_THEME); 1552 if (!accepted) { 1553 mConfiguration.setTheme(prev); 1554 selectTheme(prev); 1555 return; 1556 } else { 1557 syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, mEditedFile, mConfiguration, 1558 false, true); 1559 } 1560 } 1561 1562 saveConstraints(); 1563 } 1564 1565 void notifyFolderConfigChanged() { 1566 if (mDisableUpdates > 0 || mClient == null) { 1567 return; 1568 } 1569 1570 if (mClient.changed(CFG_FOLDER)) { 1571 saveConstraints(); 1572 } 1573 } 1574 1575 void onSelectActivity() { 1576 if (mDisableUpdates > 0) { 1577 return; 1578 } 1579 1580 String activity = (String) mActivityCombo.getData(); 1581 mConfiguration.setActivity(activity); 1582 1583 if (activity == null) { 1584 return; 1585 } 1586 1587 // See if there is a default theme assigned to this activity, and if so, use it 1588 ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject()); 1589 Map<String, String> activityThemes = manifest.getActivityThemes(); 1590 String preferred = activityThemes.get(activity); 1591 if (preferred != null && !Objects.equal(preferred, mConfiguration.getTheme())) { 1592 // Yes, switch to it 1593 selectTheme(preferred); 1594 onThemeChange(); 1595 } 1596 1597 // Persist in XML 1598 if (mClient != null) { 1599 mClient.setActivity(activity); 1600 } 1601 1602 saveConstraints(); 1603 } 1604 1605 /** 1606 * Call back for api level combo selection 1607 */ 1608 void onRenderingTargetChange() { 1609 // because mApiCombo triggers onApiLevelChange at each modification, the filling 1610 // of the combo with data will trigger notifications, and we don't want that. 1611 if (mDisableUpdates > 0) { 1612 return; 1613 } 1614 1615 IAndroidTarget prevTarget = mConfiguration.getTarget(); 1616 String prevTheme = mConfiguration.getTheme(); 1617 1618 int changeFlags = 0; 1619 1620 // tell the listener a new rendering target is being set. Need to do this before updating 1621 // mRenderingTarget. 1622 if (prevTarget != null) { 1623 changeFlags |= CFG_TARGET; 1624 mClient.aboutToChange(changeFlags); 1625 } 1626 1627 IAndroidTarget target = (IAndroidTarget) mTargetCombo.getData(); 1628 mConfiguration.setTarget(target, true); 1629 1630 // force a theme update to reflect the new rendering target. 1631 // This must be done after computeCurrentConfig since it'll depend on the currentConfig 1632 // to figure out the theme list. 1633 String oldTheme = mConfiguration.getTheme(); 1634 updateThemes(); 1635 // updateThemes may change the theme (based on theme availability in the new rendering 1636 // target) so mark theme change if necessary 1637 if (!Objects.equal(oldTheme, mConfiguration.getTheme())) { 1638 changeFlags |= CFG_THEME; 1639 } 1640 1641 if (target != null) { 1642 changeFlags |= CFG_TARGET; 1643 changeFlags |= CFG_FOLDER; // In case we added a -vNN qualifier 1644 } 1645 1646 // Store project-wide render-target setting 1647 mConfiguration.saveRenderState(); 1648 1649 mConfiguration.syncFolderConfig(); 1650 1651 if (mClient != null) { 1652 boolean accepted = mClient.changed(changeFlags); 1653 if (!accepted) { 1654 mConfiguration.setTarget(prevTarget, true); 1655 mConfiguration.setTheme(prevTheme); 1656 mConfiguration.syncFolderConfig(); 1657 selectTheme(prevTheme); 1658 selectTarget(prevTarget); 1659 } 1660 } 1661 } 1662 1663 /** 1664 * Syncs this configuration to the project wide locale and render target settings. The 1665 * locale may ignore the project-wide setting if it is a locale-specific 1666 * configuration. 1667 * 1668 * @return true if one or both of the toggles were changed, false if there were no 1669 * changes 1670 */ 1671 public boolean syncRenderState() { 1672 if (mConfiguration.getEditedConfig() == null) { 1673 // Startup; ignore 1674 return false; 1675 } 1676 1677 boolean renderTargetChanged = false; 1678 1679 // When a page is re-activated, force the toggles to reflect the current project 1680 // state 1681 1682 Pair<Locale, IAndroidTarget> pair = Configuration.loadRenderState(this); 1683 1684 int changeFlags = 0; 1685 // Only sync the locale if this layout is not already a locale-specific layout! 1686 if (pair != null && !mConfiguration.isLocaleSpecificLayout()) { 1687 Locale locale = pair.getFirst(); 1688 if (locale != null) { 1689 boolean localeChanged = setLocale(locale); 1690 if (localeChanged) { 1691 changeFlags |= CFG_LOCALE; 1692 } 1693 } else { 1694 locale = Locale.ANY; 1695 } 1696 mConfiguration.setLocale(locale, true); 1697 } 1698 1699 // Sync render target 1700 IAndroidTarget configurationTarget = mConfiguration.getTarget(); 1701 IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget; 1702 if (target != null && configurationTarget != target) { 1703 if (mClient != null && configurationTarget != null) { 1704 changeFlags |= CFG_TARGET; 1705 mClient.aboutToChange(changeFlags); 1706 } 1707 1708 mConfiguration.setTarget(target, true); 1709 selectTarget(target); 1710 renderTargetChanged = true; 1711 } 1712 1713 // Neither locale nor render target changed: nothing to do 1714 if (changeFlags == 0) { 1715 return false; 1716 } 1717 1718 // Update the locale and/or the render target. This code contains a logical 1719 // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined 1720 // such that we don't duplicate work. 1721 1722 // Compute the new configuration; we want to do this both for locale changes 1723 // and for render targets. 1724 mConfiguration.syncFolderConfig(); 1725 changeFlags |= CFG_FOLDER; // in case we added/remove a -v<NN> qualifier 1726 1727 if (renderTargetChanged) { 1728 // force a theme update to reflect the new rendering target. 1729 // This must be done after computeCurrentConfig since it'll depend on the currentConfig 1730 // to figure out the theme list. 1731 updateThemes(); 1732 } 1733 1734 if (mClient != null) { 1735 mClient.changed(changeFlags); 1736 } 1737 1738 return true; 1739 } 1740 1741 // ---- Populate data structures with themes, locales, etc ---- 1742 1743 /** 1744 * Updates the internal list of themes. 1745 */ 1746 private void updateThemes() { 1747 if (mClient == null) { 1748 return; // can't do anything without it. 1749 } 1750 1751 ResourceRepository frameworkRes = mClient.getFrameworkResources( 1752 mConfiguration.getTarget()); 1753 1754 mDisableUpdates++; 1755 1756 try { 1757 if (mEditedFile != null) { 1758 String theme = mConfiguration.getTheme(); 1759 if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) { 1760 mConfiguration.setTheme(null); 1761 mConfiguration.computePreferredTheme(); 1762 } 1763 assert mConfiguration.getTheme() != null; 1764 } 1765 1766 mThemeList.clear(); 1767 1768 ArrayList<String> themes = new ArrayList<String>(); 1769 ResourceRepository projectRes = mClient.getProjectResources(); 1770 // in cases where the opened file is not linked to a project, this could be null. 1771 if (projectRes != null) { 1772 // get the configured resources for the project 1773 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = 1774 mClient.getConfiguredProjectResources(); 1775 1776 if (configuredProjectRes != null) { 1777 // get the styles. 1778 Map<String, ResourceValue> styleMap = configuredProjectRes.get( 1779 ResourceType.STYLE); 1780 1781 if (styleMap != null) { 1782 // collect the themes out of all the styles, ie styles that extend, 1783 // directly or indirectly a platform theme. 1784 for (ResourceValue value : styleMap.values()) { 1785 if (isTheme(value, styleMap, null)) { 1786 String theme = value.getName(); 1787 themes.add(theme); 1788 } 1789 } 1790 1791 Collections.sort(themes); 1792 1793 for (String theme : themes) { 1794 if (!theme.startsWith(PREFIX_RESOURCE_REF)) { 1795 theme = STYLE_RESOURCE_PREFIX + theme; 1796 } 1797 mThemeList.add(theme); 1798 } 1799 } 1800 } 1801 themes.clear(); 1802 } 1803 1804 // get the themes, and languages from the Framework. 1805 if (frameworkRes != null) { 1806 // get the configured resources for the framework 1807 Map<ResourceType, Map<String, ResourceValue>> frameworResources = 1808 frameworkRes.getConfiguredResources(mConfiguration.getFullConfig()); 1809 1810 if (frameworResources != null) { 1811 // get the styles. 1812 Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE); 1813 1814 // collect the themes out of all the styles. 1815 for (ResourceValue value : styles.values()) { 1816 String name = value.getName(); 1817 if (name.startsWith("Theme.") || name.equals("Theme")) { //$NON-NLS-1$ //$NON-NLS-2$ 1818 themes.add(value.getName()); 1819 } 1820 } 1821 1822 // sort them and add them to the combo 1823 Collections.sort(themes); 1824 1825 for (String theme : themes) { 1826 if (!theme.startsWith(PREFIX_RESOURCE_REF)) { 1827 theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; 1828 } 1829 mThemeList.add(theme); 1830 } 1831 1832 themes.clear(); 1833 } 1834 } 1835 1836 // Migration: In the past we didn't store the style prefix in the settings; 1837 // this meant we might lose track of whether the theme is a project style 1838 // or a framework style. For now we need to migrate. Search through the 1839 // theme list until we have a match 1840 String theme = mConfiguration.getTheme(); 1841 if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) { 1842 String projectStyle = STYLE_RESOURCE_PREFIX + theme; 1843 String frameworkStyle = ANDROID_STYLE_RESOURCE_PREFIX + theme; 1844 for (String t : mThemeList) { 1845 if (t.equals(projectStyle)) { 1846 mConfiguration.setTheme(projectStyle); 1847 break; 1848 } else if (t.equals(frameworkStyle)) { 1849 mConfiguration.setTheme(frameworkStyle); 1850 break; 1851 } 1852 } 1853 if (!theme.startsWith(PREFIX_RESOURCE_REF)) { 1854 // Arbitrary guess 1855 if (theme.startsWith("Theme.")) { 1856 theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; 1857 } else { 1858 theme = STYLE_RESOURCE_PREFIX + theme; 1859 } 1860 } 1861 } 1862 1863 // TODO: Handle the case where you have a theme persisted that isn't available?? 1864 // We could look up mConfiguration.theme and make sure it appears in the list! And if 1865 // not, picking one. 1866 selectTheme(mConfiguration.getTheme()); 1867 } finally { 1868 mDisableUpdates--; 1869 } 1870 } 1871 1872 private void updateActivity() { 1873 if (mEditedFile != null) { 1874 String preferred = getPreferredActivity(mEditedFile); 1875 selectActivity(preferred); 1876 } 1877 } 1878 1879 /** 1880 * Updates the locale combo. 1881 * This must be called from the UI thread. 1882 */ 1883 public void updateLocales() { 1884 if (mClient == null) { 1885 return; // can't do anything w/o it. 1886 } 1887 1888 mDisableUpdates++; 1889 1890 try { 1891 mLocaleList.clear(); 1892 1893 SortedSet<String> languages = null; 1894 1895 // get the languages from the project. 1896 ResourceRepository projectRes = mClient.getProjectResources(); 1897 1898 // in cases where the opened file is not linked to a project, this could be null. 1899 if (projectRes != null) { 1900 // now get the languages from the project. 1901 languages = projectRes.getLanguages(); 1902 1903 for (String language : languages) { 1904 LanguageQualifier langQual = new LanguageQualifier(language); 1905 1906 // find the matching regions and add them 1907 SortedSet<String> regions = projectRes.getRegions(language); 1908 for (String region : regions) { 1909 RegionQualifier regionQual = new RegionQualifier(region); 1910 mLocaleList.add(Locale.create(langQual, regionQual)); 1911 } 1912 1913 // now the entry for the other regions the language alone 1914 // create a region qualifier that will never be matched by qualified resources. 1915 mLocaleList.add(Locale.create(langQual)); 1916 } 1917 } 1918 1919 // create language/region qualifier that will never be matched by qualified resources. 1920 mLocaleList.add(Locale.ANY); 1921 1922 Locale locale = mConfiguration.getLocale(); 1923 setLocale(locale); 1924 } finally { 1925 mDisableUpdates--; 1926 } 1927 } 1928 1929 @Nullable 1930 private String getPreferredActivity(@NonNull IFile file) { 1931 // Store/restore the activity context in the config state to help with 1932 // performance if for some reason we can't write it into the XML file and to 1933 // avoid having to open the model below 1934 if (mConfiguration.getActivity() != null) { 1935 return mConfiguration.getActivity(); 1936 } 1937 1938 IProject project = file.getProject(); 1939 1940 // Look up from XML file 1941 Document document = DomUtilities.getDocument(file); 1942 if (document != null) { 1943 Element element = document.getDocumentElement(); 1944 if (element != null) { 1945 String activity = element.getAttributeNS(TOOLS_URI, ATTR_CONTEXT); 1946 if (activity != null && !activity.isEmpty()) { 1947 if (activity.startsWith(".") || activity.indexOf('.') == -1) { //$NON-NLS-1$ 1948 ManifestInfo manifest = ManifestInfo.get(project); 1949 String pkg = manifest.getPackage(); 1950 if (!pkg.isEmpty()) { 1951 if (activity.startsWith(".")) { //$NON-NLS-1$ 1952 activity = pkg + activity; 1953 } else { 1954 activity = activity + '.' + pkg; 1955 } 1956 } 1957 } 1958 1959 mConfiguration.setActivity(activity); 1960 saveConstraints(); 1961 return activity; 1962 } 1963 } 1964 } 1965 1966 // No, not available there: try to infer it from the code index 1967 String includedIn = null; 1968 Reference includedWithin = mClient.getIncludedWithin(); 1969 if (mClient != null && includedWithin != null) { 1970 includedIn = includedWithin.getName(); 1971 } 1972 1973 ManifestInfo manifest = ManifestInfo.get(project); 1974 String pkg = manifest.getPackage(); 1975 String layoutName = ResourceHelper.getLayoutName(mEditedFile); 1976 1977 // If we are rendering a layout in included context, pick the theme 1978 // from the outer layout instead 1979 if (includedIn != null) { 1980 layoutName = includedIn; 1981 } 1982 1983 String activity = ManifestInfo.guessActivity(project, layoutName, pkg); 1984 1985 if (activity == null) { 1986 List<String> activities = ManifestInfo.getProjectActivities(project); 1987 if (activities.size() == 1) { 1988 activity = activities.get(0); 1989 } 1990 } 1991 1992 if (activity != null) { 1993 mConfiguration.setActivity(activity); 1994 saveConstraints(); 1995 return activity; 1996 } 1997 1998 // TODO: Do anything else, such as pick the first activity found? 1999 // Or just leave some default label instead? 2000 // Also, figure out what to store in the mState so I don't keep trying 2001 2002 return null; 2003 } 2004 2005 /** 2006 * Returns whether the given <var>style</var> is a theme. 2007 * This is done by making sure the parent is a theme. 2008 * @param value the style to check 2009 * @param styleMap the map of styles for the current project. Key is the style name. 2010 * @param seen the map of styles we have already processed (or null if not yet 2011 * initialized). Only the keys are significant (since there is no IdentityHashSet). 2012 * @return True if the given <var>style</var> is a theme. 2013 */ 2014 private static boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap, 2015 IdentityHashMap<ResourceValue, Boolean> seen) { 2016 if (value instanceof StyleResourceValue) { 2017 StyleResourceValue style = (StyleResourceValue)value; 2018 2019 boolean frameworkStyle = false; 2020 String parentStyle = style.getParentStyle(); 2021 if (parentStyle == null) { 2022 // if there is no specified parent style we look an implied one. 2023 // For instance 'Theme.light' is implied child style of 'Theme', 2024 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light' 2025 String name = style.getName(); 2026 int index = name.lastIndexOf('.'); 2027 if (index != -1) { 2028 parentStyle = name.substring(0, index); 2029 } 2030 } else { 2031 // remove the useless @ if it's there 2032 if (parentStyle.startsWith("@")) { 2033 parentStyle = parentStyle.substring(1); 2034 } 2035 2036 // check for framework identifier. 2037 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) { 2038 frameworkStyle = true; 2039 parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length()); 2040 } 2041 2042 // at this point we could have the format style/<name>. we want only the name 2043 if (parentStyle.startsWith("style/")) { 2044 parentStyle = parentStyle.substring("style/".length()); 2045 } 2046 } 2047 2048 if (parentStyle != null) { 2049 if (frameworkStyle) { 2050 // if the parent is a framework style, it has to be 'Theme' or 'Theme.*' 2051 return parentStyle.equals("Theme") || parentStyle.startsWith("Theme."); 2052 } else { 2053 // if it's a project style, we check this is a theme. 2054 ResourceValue parentValue = styleMap.get(parentStyle); 2055 2056 // also prevent stack overflow in case the dev mistakenly declared 2057 // the parent of the style as the style itself. 2058 if (parentValue != null && !parentValue.equals(value)) { 2059 if (seen == null) { 2060 seen = new IdentityHashMap<ResourceValue, Boolean>(); 2061 seen.put(value, Boolean.TRUE); 2062 } else if (seen.containsKey(parentValue)) { 2063 return false; 2064 } 2065 seen.put(parentValue, Boolean.TRUE); 2066 return isTheme(parentValue, styleMap, seen); 2067 } 2068 } 2069 } 2070 } 2071 2072 return false; 2073 } 2074 2075 /** 2076 * Returns true if this configuration chooser represents the best match for 2077 * the given file 2078 * 2079 * @param file the file to test 2080 * @param config the config to test 2081 * @return true if the given config is the best match for the given file 2082 */ 2083 public boolean isBestMatchFor(IFile file, FolderConfiguration config) { 2084 ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), 2085 ResourceType.LAYOUT, config); 2086 if (match != null) { 2087 return match.getFile().equals(mEditedFile); 2088 } 2089 2090 return false; 2091 } 2092 } 2093