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