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