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