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