1 /* 2 * Copyright (C) 2008 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.ide.common.layout.LayoutConstants.ANDROID_NS_NAME_PREFIX; 20 import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_STYLE; 21 22 import com.android.ide.common.api.Rect; 23 import com.android.ide.common.rendering.api.ResourceValue; 24 import com.android.ide.common.rendering.api.StyleResourceValue; 25 import com.android.ide.common.resources.ResourceFile; 26 import com.android.ide.common.resources.ResourceFolder; 27 import com.android.ide.common.resources.ResourceRepository; 28 import com.android.ide.common.resources.configuration.DensityQualifier; 29 import com.android.ide.common.resources.configuration.FolderConfiguration; 30 import com.android.ide.common.resources.configuration.LanguageQualifier; 31 import com.android.ide.common.resources.configuration.NightModeQualifier; 32 import com.android.ide.common.resources.configuration.RegionQualifier; 33 import com.android.ide.common.resources.configuration.ResourceQualifier; 34 import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; 35 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; 36 import com.android.ide.common.resources.configuration.ScreenSizeQualifier; 37 import com.android.ide.common.resources.configuration.UiModeQualifier; 38 import com.android.ide.common.resources.configuration.VersionQualifier; 39 import com.android.ide.common.sdk.LoadStatus; 40 import com.android.ide.eclipse.adt.AdtPlugin; 41 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; 42 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 43 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 44 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 45 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 46 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 47 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 48 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice; 49 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig; 50 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager; 51 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 52 import com.android.resources.Density; 53 import com.android.resources.NightMode; 54 import com.android.resources.ResourceFolderType; 55 import com.android.resources.ResourceType; 56 import com.android.resources.ScreenOrientation; 57 import com.android.resources.ScreenSize; 58 import com.android.resources.UiMode; 59 import com.android.sdklib.AndroidVersion; 60 import com.android.sdklib.IAndroidTarget; 61 import com.android.sdklib.repository.PkgProps; 62 import com.android.sdklib.util.SparseIntArray; 63 import com.android.util.Pair; 64 65 import org.eclipse.core.resources.IFile; 66 import org.eclipse.core.resources.IFolder; 67 import org.eclipse.core.resources.IProject; 68 import org.eclipse.core.runtime.CoreException; 69 import org.eclipse.core.runtime.IStatus; 70 import org.eclipse.core.runtime.QualifiedName; 71 import org.eclipse.swt.SWT; 72 import org.eclipse.swt.events.SelectionAdapter; 73 import org.eclipse.swt.events.SelectionEvent; 74 import org.eclipse.swt.layout.GridData; 75 import org.eclipse.swt.layout.GridLayout; 76 import org.eclipse.swt.widgets.Button; 77 import org.eclipse.swt.widgets.Combo; 78 import org.eclipse.swt.widgets.Composite; 79 import org.eclipse.swt.widgets.Label; 80 import org.eclipse.ui.IEditorPart; 81 import org.eclipse.ui.IWorkbench; 82 import org.eclipse.ui.IWorkbenchPage; 83 import org.eclipse.ui.IWorkbenchWindow; 84 import org.eclipse.ui.PlatformUI; 85 86 import java.util.ArrayList; 87 import java.util.Collections; 88 import java.util.Comparator; 89 import java.util.HashSet; 90 import java.util.IdentityHashMap; 91 import java.util.List; 92 import java.util.Locale; 93 import java.util.Map; 94 import java.util.Set; 95 import java.util.SortedSet; 96 97 /** 98 * A composite that displays the current configuration displayed in a Graphical Layout Editor. 99 * <p/> 100 * The composite has several entry points:<br> 101 * - {@link #setFile(IFile)}<br> 102 * Called after the constructor to set the file being edited. Nothing else is performed.<br> 103 *<br> 104 * - {@link #onXmlModelLoaded()}<br> 105 * Called when the XML model is loaded, either the first time or when the Target/SDK changes. 106 * This initializes the UI, either with the first compatible configuration found, or attempts 107 * to restore a configuration if one is found to have been saved in the file persistent storage. 108 * (see {@link #storeState()})<br> 109 *<br> 110 * - {@link #replaceFile(IFile)}<br> 111 * Called when a file, representing the same resource but with a different config is opened<br> 112 * by the user.<br> 113 *<br> 114 * - {@link #changeFileOnNewConfig(IFile)}<br> 115 * Called when config change triggers the editing of a file with a different config. 116 *<p/> 117 * Additionally, the composite can handle the following events.<br> 118 * - SDK reload. This is when the main SDK is finished loading.<br> 119 * - Target reload. This is when the target used by the project is the edited file has finished<br> 120 * loading.<br> 121 */ 122 public class ConfigurationComposite extends Composite { 123 private final static String SEP = ":"; //$NON-NLS-1$ 124 private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ 125 126 /** 127 * Setting name for project-wide setting controlling rendering target and locale which 128 * is shared for all files 129 */ 130 public final static QualifiedName NAME_RENDER_STATE = 131 new QualifiedName(AdtPlugin.PLUGIN_ID, "render");//$NON-NLS-1$ 132 133 /** 134 * Settings name for file-specific configuration preferences, such as which theme or 135 * device to render the current layout with 136 */ 137 public final static QualifiedName NAME_CONFIG_STATE = 138 new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$ 139 140 private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$ 141 142 private final static int LOCALE_LANG = 0; 143 private final static int LOCALE_REGION = 1; 144 145 private Label mCurrentLayoutLabel; 146 private Button mCreateButton; 147 148 private Combo mDeviceCombo; 149 private Combo mDeviceConfigCombo; 150 private Combo mLocaleCombo; 151 private Combo mUiModeCombo; 152 private Combo mNightCombo; 153 private Combo mThemeCombo; 154 private Combo mTargetCombo; 155 156 /** 157 * List of booleans, matching item for item the theme names in the mThemeCombo 158 * combobox, where each boolean represents whether the corresponding theme is a 159 * project theme 160 */ 161 private List<Boolean> mIsProjectTheme = new ArrayList<Boolean>(40); 162 163 /** updates are disabled if > 0 */ 164 private int mDisableUpdates = 0; 165 166 private List<LayoutDevice> mDeviceList; 167 private final List<IAndroidTarget> mTargetList = new ArrayList<IAndroidTarget>(); 168 169 private final ArrayList<ResourceQualifier[] > mLocaleList = 170 new ArrayList<ResourceQualifier[]>(); 171 172 private final ConfigState mState = new ConfigState(); 173 174 private boolean mSdkChanged = false; 175 private boolean mFirstXmlModelChange = true; 176 177 /** The config listener given to the constructor. Never null. */ 178 private final IConfigListener mListener; 179 180 /** The {@link FolderConfiguration} representing the state of the UI controls */ 181 private final FolderConfiguration mCurrentConfig = new FolderConfiguration(); 182 183 /** The file being edited */ 184 private IFile mEditedFile; 185 /** The {@link ProjectResources} for the edited file's project */ 186 private ProjectResources mResources; 187 /** The target of the project of the file being edited. */ 188 private IAndroidTarget mProjectTarget; 189 /** The target of the project of the file being edited. */ 190 private IAndroidTarget mRenderingTarget; 191 /** The {@link FolderConfiguration} being edited. */ 192 private FolderConfiguration mEditedConfig; 193 /** Serialized state to use when initializing the configuration after the SDK is loaded */ 194 private String mInitialState; 195 196 /** 197 * Interface implemented by the part which owns a {@link ConfigurationComposite}. 198 * This notifies the owners when the configuration change. 199 * The owner must also provide methods to provide the configuration that will 200 * be displayed. 201 */ 202 public interface IConfigListener { 203 /** 204 * Called when the {@link FolderConfiguration} change. The new config can be queried 205 * with {@link ConfigurationComposite#getCurrentConfig()}. 206 */ 207 void onConfigurationChange(); 208 209 /** 210 * Called after a device has changed (in addition to {@link #onConfigurationChange} 211 * getting called) 212 */ 213 void onDevicePostChange(); 214 215 /** 216 * Called when the current theme changes. The theme can be queried with 217 * {@link ConfigurationComposite#getTheme()}. 218 */ 219 void onThemeChange(); 220 221 /** 222 * Called when the "Create" button is clicked. 223 */ 224 void onCreate(); 225 226 /** 227 * Called before the rendering target changes. 228 * @param oldTarget the old rendering target 229 */ 230 void onRenderingTargetPreChange(IAndroidTarget oldTarget); 231 232 /** 233 * Called after the rendering target changes. 234 * 235 * @param target the new rendering target 236 */ 237 void onRenderingTargetPostChange(IAndroidTarget target); 238 239 ResourceRepository getProjectResources(); 240 ResourceRepository getFrameworkResources(); 241 ResourceRepository getFrameworkResources(IAndroidTarget target); 242 Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources(); 243 Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources(); 244 String getIncludedWithin(); 245 } 246 247 /** 248 * State of the current config. This is used during UI reset to attempt to return the 249 * rendering to its original configuration. 250 */ 251 private class ConfigState { 252 LayoutDevice device; 253 String configName; 254 ResourceQualifier[] locale; 255 String theme; 256 /** UI mode. Guaranteed to be non null */ 257 UiMode uiMode = UiMode.NORMAL; 258 /** night mode. Guaranteed to be non null */ 259 NightMode night = NightMode.NOTNIGHT; 260 /** the version being targeted for rendering */ 261 IAndroidTarget target; 262 263 String getData() { 264 StringBuilder sb = new StringBuilder(); 265 if (device != null) { 266 sb.append(device.getName()); 267 sb.append(SEP); 268 sb.append(configName); 269 sb.append(SEP); 270 if (isLocaleSpecificLayout() && locale != null) { 271 if (locale[0] != null && locale[1] != null) { 272 // locale[0]/[1] can be null sometimes when starting Eclipse 273 sb.append(((LanguageQualifier) locale[0]).getValue()); 274 sb.append(SEP_LOCALE); 275 sb.append(((RegionQualifier) locale[1]).getValue()); 276 } 277 } 278 sb.append(SEP); 279 sb.append(theme); 280 sb.append(SEP); 281 sb.append(uiMode.getResourceValue()); 282 sb.append(SEP); 283 sb.append(night.getResourceValue()); 284 sb.append(SEP); 285 286 // We used to store the render target here in R9. Leave a marker 287 // to ensure that we don't reuse this slot; add new extra fields after it. 288 sb.append(SEP); 289 } 290 291 return sb.toString(); 292 } 293 294 boolean setData(String data) { 295 String[] values = data.split(SEP); 296 if (values.length == 6 || values.length == 7) { 297 for (LayoutDevice d : mDeviceList) { 298 if (d.getName().equals(values[0])) { 299 device = d; 300 FolderConfiguration config = device.getFolderConfigByName(values[1]); 301 if (config != null) { 302 configName = values[1]; 303 304 // Load locale. Note that this can get overwritten by the 305 // project-wide settings read below. 306 locale = new ResourceQualifier[2]; 307 String locales[] = values[2].split(SEP_LOCALE); 308 if (locales.length >= 2) { 309 if (locales[0].length() > 0) { 310 locale[0] = new LanguageQualifier(locales[0]); 311 } 312 if (locales[1].length() > 0) { 313 locale[1] = new RegionQualifier(locales[1]); 314 } 315 } 316 317 theme = values[3]; 318 uiMode = UiMode.getEnum(values[4]); 319 if (uiMode == null) { 320 uiMode = UiMode.NORMAL; 321 } 322 night = NightMode.getEnum(values[5]); 323 if (night == null) { 324 night = NightMode.NOTNIGHT; 325 } 326 327 // element 7/values[6]: used to store render target in R9. 328 // No longer stored here. If adding more data, make 329 // sure you leave 7 alone. 330 331 Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState(); 332 333 // We only use the "global" setting 334 if (!isLocaleSpecificLayout()) { 335 locale = pair.getFirst(); 336 } 337 target = pair.getSecond(); 338 339 return true; 340 } 341 } 342 } 343 } 344 345 return false; 346 } 347 348 @Override 349 public String toString() { 350 return getData(); 351 } 352 } 353 354 /** 355 * Returns a String id to represent an {@link IAndroidTarget} which can be translated 356 * back to an {@link IAndroidTarget} by the matching {@link #stringToTarget}. The id 357 * will never contain the {@link #SEP} character. 358 * 359 * @param target the target to return an id for 360 * @return an id for the given target; never null 361 */ 362 private String targetToString(IAndroidTarget target) { 363 return target.getFullName().replace(SEP, ""); //$NON-NLS-1$ 364 } 365 366 /** 367 * Returns an {@link IAndroidTarget} that corresponds to the given id that was 368 * originally returned by {@link #targetToString}. May be null, if the platform is no 369 * longer available, or if the platform list has not yet been initialized. 370 * 371 * @param id the id that corresponds to the desired platform 372 * @return an {@link IAndroidTarget} that matches the given id, or null 373 */ 374 private IAndroidTarget stringToTarget(String id) { 375 if (mTargetList != null && mTargetList.size() > 0) { 376 for (IAndroidTarget target : mTargetList) { 377 if (id.equals(targetToString(target))) { 378 return target; 379 } 380 } 381 } 382 383 return null; 384 } 385 386 /** 387 * Creates a new {@link ConfigurationComposite} and adds it to the parent. 388 * 389 * The method also receives custom buttons to set into the configuration composite. The list 390 * is organized as an array of arrays. Each array represents a group of buttons thematically 391 * grouped together. 392 * 393 * @param listener An {@link IConfigListener} that gets and sets configuration properties. 394 * Mandatory, cannot be null. 395 * @param parent The parent composite. 396 * @param style The style of this composite. 397 * @param initialState The initial state (serialized form) to use for the configuration 398 */ 399 public ConfigurationComposite(IConfigListener listener, 400 Composite parent, int style, String initialState) { 401 super(parent, style); 402 mListener = listener; 403 mInitialState = initialState; 404 405 GridLayout gl; 406 GridData gd; 407 int cols = 7; // device+config+dock+day+separator*2+theme 408 409 // ---- First line: editing config display, locale, theme, create-button 410 Composite labelParent = new Composite(this, SWT.NONE); 411 labelParent.setLayout(gl = new GridLayout(5, false)); 412 gl.marginWidth = gl.marginHeight = 0; 413 gl.marginTop = 3; 414 labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 415 gd.horizontalSpan = cols; 416 417 new Label(labelParent, SWT.NONE).setText("Editing config:"); 418 mCurrentLayoutLabel = new Label(labelParent, SWT.NONE); 419 mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 420 gd.widthHint = 50; 421 422 mLocaleCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY); 423 mLocaleCombo.addSelectionListener(new SelectionAdapter() { 424 @Override 425 public void widgetSelected(SelectionEvent e) { 426 onLocaleChange(); 427 } 428 }); 429 430 // Layout bug workaround. Without this, in -some- scenarios the Locale combo box was 431 // coming up tiny. Setting a minimumWidth hint does not work either. We need to have 432 // 2 or more items in the locale combo box when the layout is first run. These items 433 // are removed as part of the locale initialization when the SDK is loaded. 434 mLocaleCombo.add("Locale"); //$NON-NLS-1$ // Dummy place holders 435 mLocaleCombo.add("Locale"); //$NON-NLS-1$ 436 437 mTargetCombo = new Combo(labelParent, SWT.DROP_DOWN | SWT.READ_ONLY); 438 mTargetCombo.add("Android AOSP"); //$NON-NLS-1$ // Dummy place holders 439 mTargetCombo.add("Android AOSP"); //$NON-NLS-1$ 440 mTargetCombo.addSelectionListener(new SelectionAdapter() { 441 @Override 442 public void widgetSelected(SelectionEvent e) { 443 onRenderingTargetChange(); 444 } 445 }); 446 447 mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT); 448 mCreateButton.setText("Create..."); 449 mCreateButton.setEnabled(false); 450 mCreateButton.addSelectionListener(new SelectionAdapter() { 451 @Override 452 public void widgetSelected(SelectionEvent e) { 453 if (mListener != null) { 454 mListener.onCreate(); 455 } 456 } 457 }); 458 459 // ---- 2nd line: device/config/locale/theme Combos, create button. 460 461 setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 462 setLayout(gl = new GridLayout(cols, false)); 463 gl.marginHeight = 0; 464 gl.horizontalSpacing = 0; 465 466 mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 467 mDeviceCombo.setLayoutData(new GridData( 468 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 469 mDeviceCombo.addSelectionListener(new SelectionAdapter() { 470 @Override 471 public void widgetSelected(SelectionEvent e) { 472 onDeviceChange(true /* recomputeLayout*/); 473 } 474 }); 475 476 mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 477 mDeviceConfigCombo.setLayoutData(new GridData( 478 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 479 mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() { 480 @Override 481 public void widgetSelected(SelectionEvent e) { 482 onDeviceConfigChange(); 483 } 484 }); 485 486 // first separator 487 Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL); 488 separator.setLayoutData(gd = new GridData( 489 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); 490 gd.heightHint = 0; 491 492 mUiModeCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 493 mUiModeCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL 494 | GridData.GRAB_HORIZONTAL)); 495 for (UiMode mode : UiMode.values()) { 496 mUiModeCombo.add(mode.getLongDisplayValue()); 497 } 498 mUiModeCombo.addSelectionListener(new SelectionAdapter() { 499 @Override 500 public void widgetSelected(SelectionEvent e) { 501 onDockChange(); 502 } 503 }); 504 505 mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 506 mNightCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL 507 | GridData.GRAB_HORIZONTAL)); 508 for (NightMode mode : NightMode.values()) { 509 mNightCombo.add(mode.getLongDisplayValue()); 510 } 511 mNightCombo.addSelectionListener(new SelectionAdapter() { 512 @Override 513 public void widgetSelected(SelectionEvent e) { 514 onDayChange(); 515 } 516 }); 517 518 mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN); 519 mThemeCombo.setLayoutData(new GridData( 520 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 521 mThemeCombo.setEnabled(false); 522 523 mThemeCombo.addSelectionListener(new SelectionAdapter() { 524 @Override 525 public void widgetSelected(SelectionEvent e) { 526 onThemeChange(); 527 } 528 }); 529 } 530 531 // ---- Init and reset/reload methods ---- 532 533 /** 534 * Sets the reference to the file being edited. 535 * <p/>The UI is initialized in {@link #onXmlModelLoaded()} which is called as the XML model is 536 * loaded (or reloaded as the SDK/target changes). 537 * 538 * @param file the file being opened 539 * 540 * @see #onXmlModelLoaded() 541 * @see #replaceFile(IFile) 542 * @see #changeFileOnNewConfig(IFile) 543 */ 544 public void setFile(IFile file) { 545 mEditedFile = file; 546 } 547 548 /** 549 * Replaces the UI with a given file configuration. This is meant to answer the user 550 * explicitly opening a different version of the same layout from the Package Explorer. 551 * <p/>This attempts to keep the current config, but may change it if it's not compatible or 552 * not the best match 553 * <p/>This will NOT trigger a redraw event (will not call 554 * {@link IConfigListener#onConfigurationChange()}.) 555 * @param file the file being opened. 556 */ 557 public void replaceFile(IFile file) { 558 // if there is no previous selection, revert to default mode. 559 if (mState.device == null) { 560 setFile(file); // onTargetChanged will be called later. 561 return; 562 } 563 564 mEditedFile = file; 565 IProject iProject = mEditedFile.getProject(); 566 mResources = ResourceManager.getInstance().getProjectResources(iProject); 567 568 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); 569 mEditedConfig = resFolder.getConfiguration(); 570 571 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 572 // new values in the widgets. 573 574 try { 575 // only attempt to do anything if the SDK and targets are loaded. 576 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 577 if (sdkStatus == LoadStatus.LOADED) { 578 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, 579 null /*project*/); 580 581 if (targetStatus == LoadStatus.LOADED) { 582 583 // update the current config selection to make sure it's 584 // compatible with the new file 585 adaptConfigSelection(true /*needBestMatch*/); 586 587 // compute the final current config 588 computeCurrentConfig(); 589 590 // update the string showing the config value 591 updateConfigDisplay(mEditedConfig); 592 } 593 } 594 } finally { 595 mDisableUpdates--; 596 } 597 } 598 599 /** 600 * Updates the UI with a new file that was opened in response to a config change. 601 * @param file the file being opened. 602 * 603 * @see #replaceFile(IFile) 604 */ 605 public void changeFileOnNewConfig(IFile file) { 606 mEditedFile = file; 607 IProject iProject = mEditedFile.getProject(); 608 mResources = ResourceManager.getInstance().getProjectResources(iProject); 609 610 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); 611 mEditedConfig = resFolder.getConfiguration(); 612 613 // All that's needed is to update the string showing the config value 614 // (since the config combo were chosen by the user). 615 updateConfigDisplay(mEditedConfig); 616 } 617 618 /** 619 * Responds to the event that the basic SDK information finished loading. 620 * @param target the possibly new target object associated with the file being edited (in case 621 * the SDK path was changed). 622 */ 623 public void onSdkLoaded(IAndroidTarget target) { 624 // a change to the SDK means that we need to check for new/removed devices. 625 mSdkChanged = true; 626 627 // store the new target. 628 mProjectTarget = target; 629 630 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 631 // new values in the widgets. 632 try { 633 // this is going to be followed by a call to onTargetLoaded. 634 // So we can only care about the layout devices in this case. 635 initDevices(); 636 initTargets(); 637 } finally { 638 mDisableUpdates--; 639 } 640 } 641 642 /** 643 * Answers to the XML model being loaded, either the first time or when the Target/SDK changes. 644 * <p>This initializes the UI, either with the first compatible configuration found, 645 * or attempts to restore a configuration if one is found to have been saved in the file 646 * persistent storage. 647 * <p>If the SDK or target are not loaded, nothing will happened (but the method must be called 648 * back when those are loaded). 649 * <p>The method automatically handles being called the first time after editor creation, or 650 * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)} 651 * is properly called). 652 * 653 * @see #storeState() 654 * @see #onSdkLoaded(IAndroidTarget) 655 */ 656 public AndroidTargetData onXmlModelLoaded() { 657 AndroidTargetData targetData = null; 658 659 // only attempt to do anything if the SDK and targets are loaded. 660 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 661 if (sdkStatus == LoadStatus.LOADED) { 662 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 663 664 try { 665 // init the devices if needed (new SDK or first time going through here) 666 if (mSdkChanged || mFirstXmlModelChange) { 667 initDevices(); 668 initTargets(); 669 } 670 671 IProject iProject = mEditedFile.getProject(); 672 673 Sdk currentSdk = Sdk.getCurrent(); 674 if (currentSdk != null) { 675 mProjectTarget = currentSdk.getTarget(iProject); 676 } 677 678 LoadStatus targetStatus = LoadStatus.FAILED; 679 if (mProjectTarget != null) { 680 targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null); 681 initTargets(); 682 } 683 684 if (targetStatus == LoadStatus.LOADED) { 685 if (mResources == null) { 686 mResources = ResourceManager.getInstance().getProjectResources(iProject); 687 } 688 if (mEditedConfig == null) { 689 ResourceFolder resFolder = mResources.getResourceFolder( 690 (IFolder) mEditedFile.getParent()); 691 mEditedConfig = resFolder.getConfiguration(); 692 } 693 694 targetData = Sdk.getCurrent().getTargetData(mProjectTarget); 695 696 // get the file stored state 697 boolean loadedConfigData = false; 698 String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE); 699 if (mInitialState != null) { 700 data = mInitialState; 701 mInitialState = null; 702 } 703 if (data != null) { 704 loadedConfigData = mState.setData(data); 705 } 706 707 updateLocales(); 708 709 // If the current state was loaded from the persistent storage, we update the 710 // UI with it and then try to adapt it (which will handle incompatible 711 // configuration). 712 // Otherwise, just look for the first compatible configuration. 713 if (loadedConfigData) { 714 // first make sure we have the config to adapt 715 selectDevice(mState.device); 716 fillConfigCombo(mState.configName); 717 718 adaptConfigSelection(false /*needBestMatch*/); 719 720 mUiModeCombo.select(UiMode.getIndex(mState.uiMode)); 721 mNightCombo.select(NightMode.getIndex(mState.night)); 722 mTargetCombo.select(mTargetList.indexOf(mState.target)); 723 724 targetData = Sdk.getCurrent().getTargetData(mState.target); 725 } else { 726 findAndSetCompatibleConfig(false /*favorCurrentConfig*/); 727 728 // Default to modern layout lib 729 IAndroidTarget target = findDefaultRenderTarget(); 730 if (target != null) { 731 targetData = Sdk.getCurrent().getTargetData(target); 732 mTargetCombo.select(mTargetList.indexOf(target)); 733 } 734 } 735 736 // Update themes. This is done after updating the devices above, 737 // since we want to look at the chosen device size to decide 738 // what the default theme (for example, with Honeycomb we choose 739 // Holo as the default theme but only if the screen size is XLARGE 740 // (and of course only if the manifest does not specify another 741 // default theme). 742 updateThemes(); 743 744 // update the string showing the config value 745 updateConfigDisplay(mEditedConfig); 746 747 // compute the final current config 748 computeCurrentConfig(); 749 } 750 } finally { 751 mDisableUpdates--; 752 mFirstXmlModelChange = false; 753 } 754 } 755 756 return targetData; 757 } 758 759 /** Return the default render target to use, or null if no strong preference */ 760 private IAndroidTarget findDefaultRenderTarget() { 761 // Default to layoutlib version 5 762 Sdk current = Sdk.getCurrent(); 763 if (current != null) { 764 IAndroidTarget projectTarget = current.getTarget(mEditedFile.getProject()); 765 int minProjectApi = Integer.MAX_VALUE; 766 if (projectTarget != null) { 767 if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) { 768 // Renderable non-platform targets are all going to be adequate (they 769 // will have at least version 5 of layoutlib) so use the project 770 // target as the render target. 771 return projectTarget; 772 } 773 774 if (projectTarget.getVersion().isPreview() 775 && projectTarget.hasRenderingLibrary()) { 776 // If the project target is a preview version, then just use it 777 return projectTarget; 778 } 779 780 minProjectApi = projectTarget.getVersion().getApiLevel(); 781 } 782 783 // We want to pick a render target that contains at least version 5 (and 784 // preferably version 6) of the layout library. To do this, we go through the 785 // targets and pick the -smallest- API level that is both simultaneously at 786 // least as big as the project API level, and supports layoutlib level 5+. 787 IAndroidTarget best = null; 788 int bestApiLevel = Integer.MAX_VALUE; 789 790 for (IAndroidTarget target : current.getTargets()) { 791 // Non-platform targets are not chosen as the default render target 792 if (!target.isPlatform()) { 793 continue; 794 } 795 796 int apiLevel = target.getVersion().getApiLevel(); 797 798 // Ignore targets that have a lower API level than the minimum project 799 // API level: 800 if (apiLevel < minProjectApi) { 801 continue; 802 } 803 804 // Look up the layout lib API level. This property is new so it will only 805 // be defined for version 6 or higher, which means non-null is adequate 806 // to see if this target is eligible: 807 String property = target.getProperty(PkgProps.LAYOUTLIB_API); 808 // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate: 809 if (property != null || apiLevel >= 11) { 810 if (apiLevel < bestApiLevel) { 811 bestApiLevel = apiLevel; 812 best = target; 813 } 814 } 815 } 816 817 return best; 818 } 819 820 return null; 821 } 822 823 private static class ConfigBundle { 824 FolderConfiguration config; 825 int localeIndex; 826 int dockModeIndex; 827 int nightModeIndex; 828 829 ConfigBundle() { 830 config = new FolderConfiguration(); 831 localeIndex = 0; 832 dockModeIndex = 0; 833 nightModeIndex = 0; 834 } 835 836 ConfigBundle(ConfigBundle bundle) { 837 config = new FolderConfiguration(); 838 config.set(bundle.config); 839 localeIndex = bundle.localeIndex; 840 dockModeIndex = bundle.dockModeIndex; 841 nightModeIndex = bundle.nightModeIndex; 842 } 843 } 844 845 private static class ConfigMatch { 846 final FolderConfiguration testConfig; 847 final LayoutDevice device; 848 final String name; 849 final ConfigBundle bundle; 850 851 public ConfigMatch(FolderConfiguration testConfig, 852 LayoutDevice device, String name, ConfigBundle bundle) { 853 this.testConfig = testConfig; 854 this.device = device; 855 this.name = name; 856 this.bundle = bundle; 857 } 858 859 @Override 860 public String toString() { 861 return device.getName() + " - " + name; 862 } 863 } 864 865 /** 866 * Finds a device/config that can display {@link #mEditedConfig}. 867 * <p/>Once found the device and config combos are set to the config. 868 * <p/>If there is no compatible configuration, a custom one is created. 869 * @param favorCurrentConfig if true, and no best match is found, don't change 870 * the current config. This must only be true if the current config is compatible. 871 */ 872 private void findAndSetCompatibleConfig(boolean favorCurrentConfig) { 873 // list of compatible device/config/locale 874 List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>(); 875 876 // list of actual best match (ie the file is a best match for the device/config) 877 List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>(); 878 879 // get a locale that match the host locale roughly (may not be exact match on the region.) 880 int localeHostMatch = getLocaleMatch(); 881 882 // build a list of combinations of non standard qualifiers to add to each device's 883 // qualifier set when testing for a match. 884 // These qualifiers are: locale, night-mode, car dock. 885 List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200); 886 887 // If the edited file has locales, then we have to select a matching locale from 888 // the list. 889 // However, if it doesn't, we don't randomly take the first locale, we take one 890 // matching the current host locale (making sure it actually exist in the project) 891 int start, max; 892 if (mEditedConfig.getLanguageQualifier() != null || localeHostMatch == -1) { 893 // add all the locales 894 start = 0; 895 max = mLocaleList.size(); 896 } else { 897 // only add the locale host match 898 start = localeHostMatch; 899 max = localeHostMatch + 1; // test is < 900 } 901 902 for (int i = start ; i < max ; i++) { 903 ResourceQualifier[] l = mLocaleList.get(i); 904 905 ConfigBundle bundle = new ConfigBundle(); 906 bundle.config.setLanguageQualifier((LanguageQualifier) l[LOCALE_LANG]); 907 bundle.config.setRegionQualifier((RegionQualifier) l[LOCALE_REGION]); 908 909 bundle.localeIndex = i; 910 configBundles.add(bundle); 911 } 912 913 // add the dock mode to the bundle combinations. 914 addDockModeToBundles(configBundles); 915 916 // add the night mode to the bundle combinations. 917 addNightModeToBundles(configBundles); 918 919 addRenderTargetToBundles(configBundles); 920 921 for (LayoutDevice device : mDeviceList) { 922 for (DeviceConfig config : device.getConfigs()) { 923 924 // loop on the list of config bundles to create full configurations. 925 for (ConfigBundle bundle : configBundles) { 926 // create a new config with device config 927 FolderConfiguration testConfig = new FolderConfiguration(); 928 testConfig.set(config.getConfig()); 929 930 // add on top of it, the extra qualifiers from the bundle 931 testConfig.add(bundle.config); 932 933 if (mEditedConfig.isMatchFor(testConfig)) { 934 // this is a basic match. record it in case we don't find a match 935 // where the edited file is a best config. 936 anyMatches.add(new ConfigMatch(testConfig, device, config.getName(), 937 bundle)); 938 939 if (isCurrentFileBestMatchFor(testConfig)) { 940 // this is what we want. 941 bestMatches.add(new ConfigMatch(testConfig, device, config.getName(), 942 bundle)); 943 } 944 } 945 } 946 } 947 } 948 949 if (bestMatches.size() == 0) { 950 if (favorCurrentConfig) { 951 // quick check 952 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) { 953 AdtPlugin.log(IStatus.ERROR, 954 "favorCurrentConfig can only be true if the current config is compatible"); 955 } 956 957 // just display the warning 958 AdtPlugin.printErrorToConsole(mEditedFile.getProject(), 959 String.format( 960 "'%1$s' is not a best match for any device/locale combination.", 961 mEditedConfig.toDisplayString()), 962 String.format( 963 "Displaying it with '%1$s'", 964 mCurrentConfig.toDisplayString())); 965 } else if (anyMatches.size() > 0) { 966 // select the best device anyway. 967 ConfigMatch match = selectConfigMatch(anyMatches); 968 selectDevice(mState.device = match.device); 969 fillConfigCombo(match.name); 970 mLocaleCombo.select(match.bundle.localeIndex); 971 mUiModeCombo.select(match.bundle.dockModeIndex); 972 mNightCombo.select(match.bundle.nightModeIndex); 973 974 // TODO: display a better warning! 975 computeCurrentConfig(); 976 AdtPlugin.printErrorToConsole(mEditedFile.getProject(), 977 String.format( 978 "'%1$s' is not a best match for any device/locale combination.", 979 mEditedConfig.toDisplayString()), 980 String.format( 981 "Displaying it with '%1$s' which is compatible, but will actually be displayed with another more specific version of the layout.", 982 mCurrentConfig.toDisplayString())); 983 984 } else { 985 // TODO: there is no device/config able to display the layout, create one. 986 // For the base config values, we'll take the first device and config, 987 // and replace whatever qualifier required by the layout file. 988 } 989 } else { 990 ConfigMatch match = selectConfigMatch(bestMatches); 991 selectDevice(mState.device = match.device); 992 fillConfigCombo(match.name); 993 mLocaleCombo.select(match.bundle.localeIndex); 994 mUiModeCombo.select(match.bundle.dockModeIndex); 995 mNightCombo.select(match.bundle.nightModeIndex); 996 } 997 } 998 999 /** 1000 * Note: this comparator imposes orderings that are inconsistent with equals. 1001 */ 1002 private static class TabletConfigComparator implements Comparator<ConfigMatch> { 1003 @Override 1004 public int compare(ConfigMatch o1, ConfigMatch o2) { 1005 ScreenSize ss1 = o1.testConfig.getScreenSizeQualifier().getValue(); 1006 ScreenSize ss2 = o2.testConfig.getScreenSizeQualifier().getValue(); 1007 1008 // X-LARGE is better than all others (which are considered identical) 1009 // if both X-LARGE, then LANDSCAPE is better than all others (which are identical) 1010 1011 if (ss1 == ScreenSize.XLARGE) { 1012 if (ss2 == ScreenSize.XLARGE) { 1013 ScreenOrientation so1 = 1014 o1.testConfig.getScreenOrientationQualifier().getValue(); 1015 ScreenOrientation so2 = 1016 o2.testConfig.getScreenOrientationQualifier().getValue(); 1017 1018 if (so1 == ScreenOrientation.LANDSCAPE) { 1019 if (so2 == ScreenOrientation.LANDSCAPE) { 1020 return 0; 1021 } else { 1022 return -1; 1023 } 1024 } else if (so2 == ScreenOrientation.LANDSCAPE) { 1025 return 1; 1026 } else { 1027 return 0; 1028 } 1029 } else { 1030 return -1; 1031 } 1032 } else if (ss2 == ScreenSize.XLARGE) { 1033 return 1; 1034 } else { 1035 return 0; 1036 } 1037 } 1038 } 1039 1040 /** 1041 * Note: this comparator imposes orderings that are inconsistent with equals. 1042 */ 1043 private static class PhoneConfigComparator implements Comparator<ConfigMatch> { 1044 1045 private SparseIntArray mDensitySort = new SparseIntArray(4); 1046 1047 public PhoneConfigComparator() { 1048 // put the sort order for the density. 1049 mDensitySort.put(Density.HIGH.getDpiValue(), 1); 1050 mDensitySort.put(Density.MEDIUM.getDpiValue(), 2); 1051 mDensitySort.put(Density.XHIGH.getDpiValue(), 3); 1052 mDensitySort.put(Density.LOW.getDpiValue(), 4); 1053 } 1054 1055 @Override 1056 public int compare(ConfigMatch o1, ConfigMatch o2) { 1057 int dpi1 = Density.DEFAULT_DENSITY; 1058 if (o1.testConfig.getDensityQualifier() != null) { 1059 dpi1 = o1.testConfig.getDensityQualifier().getValue().getDpiValue(); 1060 dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/); 1061 } 1062 1063 int dpi2 = Density.DEFAULT_DENSITY; 1064 if (o2.testConfig.getDensityQualifier() != null) { 1065 dpi2 = o2.testConfig.getDensityQualifier().getValue().getDpiValue(); 1066 dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/); 1067 } 1068 1069 if (dpi1 == dpi2) { 1070 // portrait is better 1071 ScreenOrientation so1 = 1072 o1.testConfig.getScreenOrientationQualifier().getValue(); 1073 ScreenOrientation so2 = 1074 o2.testConfig.getScreenOrientationQualifier().getValue(); 1075 1076 if (so1 == ScreenOrientation.PORTRAIT) { 1077 if (so2 == ScreenOrientation.PORTRAIT) { 1078 return 0; 1079 } else { 1080 return -1; 1081 } 1082 } else if (so2 == ScreenOrientation.PORTRAIT) { 1083 return 1; 1084 } else { 1085 return 0; 1086 } 1087 } 1088 1089 return dpi1 - dpi2; 1090 } 1091 } 1092 1093 private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { 1094 // API 11-13: look for a x-large device 1095 int apiLevel = mProjectTarget.getVersion().getApiLevel(); 1096 if (apiLevel >= 11 && apiLevel < 14) { 1097 // TODO: Maybe check the compatible-screen tag in the manifest to figure out 1098 // what kind of device should be used for display. 1099 Collections.sort(matches, new TabletConfigComparator()); 1100 } else { 1101 // lets look for a high density device 1102 Collections.sort(matches, new PhoneConfigComparator()); 1103 } 1104 1105 // Look at the currently active editor to see if it's a layout editor, and if so, 1106 // look up its configuration and if the configuration is in our match list, 1107 // use it. This means we "preserve" the current configuration when you open 1108 // new layouts. 1109 IWorkbench workbench = PlatformUI.getWorkbench(); 1110 IWorkbenchWindow activeWorkbenchWindow = workbench.getActiveWorkbenchWindow(); 1111 IWorkbenchPage page = activeWorkbenchWindow.getActivePage(); 1112 IEditorPart activeEditor = page.getActiveEditor(); 1113 LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); 1114 if (delegate != null 1115 && mEditedFile != null 1116 // (Only do this when the two files are in the same project) 1117 && delegate.getEditor().getProject() == mEditedFile.getProject()) { 1118 FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration(); 1119 if (configuration != null) { 1120 for (ConfigMatch match : matches) { 1121 if (configuration.equals(match.testConfig)) { 1122 return match; 1123 } 1124 } 1125 } 1126 } 1127 1128 // the list has been sorted so that the first item is the best config 1129 return matches.get(0); 1130 } 1131 1132 private void addRenderTargetToBundles(List<ConfigBundle> configBundles) { 1133 Pair<ResourceQualifier[], IAndroidTarget> state = loadRenderState(); 1134 if (state != null) { 1135 IAndroidTarget target = state.getSecond(); 1136 if (target != null) { 1137 int apiLevel = target.getVersion().getApiLevel(); 1138 for (ConfigBundle bundle : configBundles) { 1139 bundle.config.setVersionQualifier( 1140 new VersionQualifier(apiLevel)); 1141 } 1142 } 1143 } 1144 } 1145 1146 private void addDockModeToBundles(List<ConfigBundle> addConfig) { 1147 ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); 1148 1149 // loop on each item and for each, add all variations of the dock modes 1150 for (ConfigBundle bundle : addConfig) { 1151 int index = 0; 1152 for (UiMode mode : UiMode.values()) { 1153 ConfigBundle b = new ConfigBundle(bundle); 1154 b.config.setUiModeQualifier(new UiModeQualifier(mode)); 1155 b.dockModeIndex = index++; 1156 list.add(b); 1157 } 1158 } 1159 1160 addConfig.clear(); 1161 addConfig.addAll(list); 1162 } 1163 1164 private void addNightModeToBundles(List<ConfigBundle> addConfig) { 1165 ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); 1166 1167 // loop on each item and for each, add all variations of the night modes 1168 for (ConfigBundle bundle : addConfig) { 1169 int index = 0; 1170 for (NightMode mode : NightMode.values()) { 1171 ConfigBundle b = new ConfigBundle(bundle); 1172 b.config.setNightModeQualifier(new NightModeQualifier(mode)); 1173 b.nightModeIndex = index++; 1174 list.add(b); 1175 } 1176 } 1177 1178 addConfig.clear(); 1179 addConfig.addAll(list); 1180 } 1181 1182 /** 1183 * Adapts the current device/config selection so that it's compatible with 1184 * {@link #mEditedConfig}. 1185 * <p/>If the current selection is compatible, nothing is changed. 1186 * <p/>If it's not compatible, configs from the current devices are tested. 1187 * <p/>If none are compatible, it reverts to 1188 * {@link #findAndSetCompatibleConfig(FolderConfiguration)} 1189 */ 1190 private void adaptConfigSelection(boolean needBestMatch) { 1191 // check the device config (ie sans locale) 1192 boolean needConfigChange = true; // if still true, we need to find another config. 1193 boolean currentConfigIsCompatible = false; 1194 int configIndex = mDeviceConfigCombo.getSelectionIndex(); 1195 if (configIndex != -1) { 1196 String configName = mDeviceConfigCombo.getItem(configIndex); 1197 FolderConfiguration currentConfig = mState.device.getFolderConfigByName(configName); 1198 if (currentConfig != null && mEditedConfig.isMatchFor(currentConfig)) { 1199 currentConfigIsCompatible = true; // current config is compatible 1200 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) { 1201 needConfigChange = false; 1202 } 1203 } 1204 } 1205 1206 if (needConfigChange) { 1207 // if the current config/locale isn't a correct match, then 1208 // look for another config/locale in the same device. 1209 FolderConfiguration testConfig = new FolderConfiguration(); 1210 1211 // first look in the current device. 1212 String matchName = null; 1213 int localeIndex = -1; 1214 mainloop: for (DeviceConfig config : mState.device.getConfigs()) { 1215 testConfig.set(config.getConfig()); 1216 1217 // loop on the locales. 1218 for (int i = 0 ; i < mLocaleList.size() ; i++) { 1219 ResourceQualifier[] locale = mLocaleList.get(i); 1220 1221 // update the test config with the locale qualifiers 1222 testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]); 1223 testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]); 1224 1225 if (mEditedConfig.isMatchFor(testConfig) && 1226 isCurrentFileBestMatchFor(testConfig)) { 1227 matchName = config.getName(); 1228 localeIndex = i; 1229 break mainloop; 1230 } 1231 } 1232 } 1233 1234 if (matchName != null) { 1235 selectConfig(matchName); 1236 mLocaleCombo.select(localeIndex); 1237 } else { 1238 // no match in current device with any config/locale 1239 // attempt to find another device that can display this particular config. 1240 findAndSetCompatibleConfig(currentConfigIsCompatible); 1241 } 1242 } 1243 } 1244 1245 /** 1246 * Finds a locale matching the config from a file. 1247 * @param language the language qualifier or null if none is set. 1248 * @param region the region qualifier or null if none is set. 1249 * @return true if there was a change in the combobox as a result of applying the locale 1250 */ 1251 private boolean setLocaleCombo(ResourceQualifier language, ResourceQualifier region) { 1252 boolean changed = false; 1253 1254 // find the locale match. Since the locale list is based on the content of the 1255 // project resources there must be an exact match. 1256 // The only trick is that the region could be null in the fileConfig but in our 1257 // list of locales, this is represented as a RegionQualifier with value of 1258 // FAKE_LOCALE_VALUE. 1259 final int count = mLocaleList.size(); 1260 for (int i = 0 ; i < count ; i++) { 1261 ResourceQualifier[] locale = mLocaleList.get(i); 1262 1263 // the language qualifier in the locale list is never null. 1264 if (locale[LOCALE_LANG].equals(language)) { 1265 // region comparison is more complex, as the region could be null. 1266 if (region == null) { 1267 if (RegionQualifier.FAKE_REGION_VALUE.equals( 1268 ((RegionQualifier)locale[LOCALE_REGION]).getValue())) { 1269 // match! 1270 if (mLocaleCombo.getSelectionIndex() != i) { 1271 mLocaleCombo.select(i); 1272 changed = true; 1273 } 1274 break; 1275 } 1276 } else if (region.equals(locale[LOCALE_REGION])) { 1277 // match! 1278 if (mLocaleCombo.getSelectionIndex() != i) { 1279 mLocaleCombo.select(i); 1280 changed = true; 1281 } 1282 break; 1283 } 1284 } 1285 } 1286 1287 return changed; 1288 } 1289 1290 private void updateConfigDisplay(FolderConfiguration fileConfig) { 1291 String current = fileConfig.toDisplayString(); 1292 String layoutLabel = current != null ? current : "(Default)"; 1293 mCurrentLayoutLabel.setText(layoutLabel); 1294 mCurrentLayoutLabel.setToolTipText(layoutLabel); 1295 } 1296 1297 private void saveState() { 1298 if (mDisableUpdates == 0) { 1299 int index = mDeviceConfigCombo.getSelectionIndex(); 1300 if (index != -1) { 1301 mState.configName = mDeviceConfigCombo.getItem(index); 1302 } else { 1303 mState.configName = null; 1304 } 1305 1306 // since the locales are relative to the project, only keeping the index is enough 1307 index = mLocaleCombo.getSelectionIndex(); 1308 if (index != -1) { 1309 mState.locale = mLocaleList.get(index); 1310 } else { 1311 mState.locale = null; 1312 } 1313 1314 index = mThemeCombo.getSelectionIndex(); 1315 if (index != -1) { 1316 mState.theme = mThemeCombo.getItem(index); 1317 } 1318 1319 index = mUiModeCombo.getSelectionIndex(); 1320 if (index != -1) { 1321 mState.uiMode = UiMode.getByIndex(index); 1322 } 1323 1324 index = mNightCombo.getSelectionIndex(); 1325 if (index != -1) { 1326 mState.night = NightMode.getByIndex(index); 1327 } 1328 1329 index = mTargetCombo.getSelectionIndex(); 1330 if (index != -1) { 1331 mState.target = mTargetList.get(index); 1332 } 1333 } 1334 } 1335 1336 /** 1337 * Stores the current config selection into the edited file. 1338 */ 1339 public void storeState() { 1340 AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, mState.getData()); 1341 } 1342 1343 /** 1344 * Updates the locale combo. 1345 * This must be called from the UI thread. 1346 */ 1347 public void updateLocales() { 1348 if (mListener == null) { 1349 return; // can't do anything w/o it. 1350 } 1351 1352 mDisableUpdates++; 1353 1354 try { 1355 // Reset the combo 1356 mLocaleCombo.removeAll(); 1357 mLocaleList.clear(); 1358 1359 SortedSet<String> languages = null; 1360 boolean hasLocale = false; 1361 1362 // get the languages from the project. 1363 ResourceRepository projectRes = mListener.getProjectResources(); 1364 1365 // in cases where the opened file is not linked to a project, this could be null. 1366 if (projectRes != null) { 1367 // now get the languages from the project. 1368 languages = projectRes.getLanguages(); 1369 1370 for (String language : languages) { 1371 hasLocale = true; 1372 1373 LanguageQualifier langQual = new LanguageQualifier(language); 1374 1375 // find the matching regions and add them 1376 SortedSet<String> regions = projectRes.getRegions(language); 1377 for (String region : regions) { 1378 mLocaleCombo.add( 1379 String.format("%1$s / %2$s", language, region)); 1380 RegionQualifier regionQual = new RegionQualifier(region); 1381 mLocaleList.add(new ResourceQualifier[] { langQual, regionQual }); 1382 } 1383 1384 // now the entry for the other regions the language alone 1385 if (regions.size() > 0) { 1386 mLocaleCombo.add(String.format("%1$s / Other", language)); 1387 } else { 1388 mLocaleCombo.add(String.format("%1$s / Any", language)); 1389 } 1390 // create a region qualifier that will never be matched by qualified resources. 1391 mLocaleList.add(new ResourceQualifier[] { 1392 langQual, 1393 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) 1394 }); 1395 } 1396 } 1397 1398 // add a locale not present in the project resources. This will let the dev 1399 // tests his/her default values. 1400 if (hasLocale) { 1401 mLocaleCombo.add("Other"); 1402 } else { 1403 mLocaleCombo.add("Any locale"); 1404 } 1405 1406 // create language/region qualifier that will never be matched by qualified resources. 1407 mLocaleList.add(new ResourceQualifier[] { 1408 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), 1409 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) 1410 }); 1411 1412 if (mState.locale != null) { 1413 // FIXME: this may fails if the layout was deleted (and was the last one to have 1414 // that local. (we have other problem in this case though) 1415 setLocaleCombo(mState.locale[LOCALE_LANG], 1416 mState.locale[LOCALE_REGION]); 1417 } else { 1418 mLocaleCombo.select(0); 1419 } 1420 1421 mThemeCombo.getParent().layout(); 1422 } finally { 1423 mDisableUpdates--; 1424 } 1425 } 1426 1427 private int getLocaleMatch() { 1428 Locale locale = Locale.getDefault(); 1429 if (locale != null) { 1430 String currentLanguage = locale.getLanguage(); 1431 String currentRegion = locale.getCountry(); 1432 1433 final int count = mLocaleList.size(); 1434 for (int l = 0 ; l < count ; l++) { 1435 ResourceQualifier[] localeArray = mLocaleList.get(l); 1436 LanguageQualifier langQ = (LanguageQualifier)localeArray[LOCALE_LANG]; 1437 RegionQualifier regionQ = (RegionQualifier)localeArray[LOCALE_REGION]; 1438 1439 // there's always a ##/Other or ##/Any (which is the same, the region 1440 // contains FAKE_REGION_VALUE). If we don't find a perfect region match 1441 // we take the fake region. Since it's last in the list, this makes the 1442 // test easy. 1443 if (langQ.getValue().equals(currentLanguage) && 1444 (regionQ.getValue().equals(currentRegion) || 1445 regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) { 1446 return l; 1447 } 1448 } 1449 1450 // if no locale match the current local locale, it's likely that it is 1451 // the default one which is the last one. 1452 return count - 1; 1453 } 1454 1455 return -1; 1456 } 1457 1458 /** 1459 * Updates the theme combo. 1460 * This must be called from the UI thread. 1461 */ 1462 private void updateThemes() { 1463 if (mListener == null) { 1464 return; // can't do anything w/o it. 1465 } 1466 1467 ResourceRepository frameworkRes = mListener.getFrameworkResources(getRenderingTarget()); 1468 1469 mDisableUpdates++; 1470 1471 try { 1472 // Reset the combo 1473 mThemeCombo.removeAll(); 1474 mIsProjectTheme.clear(); 1475 1476 ArrayList<String> themes = new ArrayList<String>(); 1477 String includedIn = mListener.getIncludedWithin(); 1478 1479 // First list any themes that are declared by the manifest 1480 if (mEditedFile != null) { 1481 IProject project = mEditedFile.getProject(); 1482 ManifestInfo manifest = ManifestInfo.get(project); 1483 1484 // Look up the screen size for the current configuration 1485 ScreenSize screenSize = null; 1486 if (mState.device != null) { 1487 List<DeviceConfig> configs = mState.device.getConfigs(); 1488 for (DeviceConfig config : configs) { 1489 ScreenSizeQualifier qualifier = 1490 config.getConfig().getScreenSizeQualifier(); 1491 screenSize = qualifier.getValue(); 1492 break; 1493 } 1494 } 1495 // Look up the default/fallback theme to use for this project (which 1496 // depends on the screen size when no particular theme is specified 1497 // in the manifest) 1498 String defaultTheme = manifest.getDefaultTheme(mState.target, screenSize); 1499 1500 Map<String, String> activityThemes = manifest.getActivityThemes(); 1501 String pkg = manifest.getPackage(); 1502 String preferred = null; 1503 boolean isIncluded = includedIn != null; 1504 if (mState.theme == null || isIncluded) { 1505 String layoutName = ResourceHelper.getLayoutName(mEditedFile); 1506 1507 // If we are rendering a layout in included context, pick the theme 1508 // from the outer layout instead 1509 if (includedIn != null) { 1510 layoutName = includedIn; 1511 } 1512 1513 String activity = ManifestInfo.guessActivity(project, layoutName, pkg); 1514 if (activity != null) { 1515 preferred = activityThemes.get(activity); 1516 } 1517 if (preferred == null) { 1518 preferred = defaultTheme; 1519 } 1520 String preferredTheme = ResourceHelper.styleToTheme(preferred); 1521 if (includedIn == null) { 1522 mState.theme = preferredTheme; 1523 } 1524 boolean isProjectTheme = !preferred.startsWith(PREFIX_ANDROID_STYLE); 1525 mThemeCombo.add(preferredTheme); 1526 mIsProjectTheme.add(Boolean.valueOf(isProjectTheme)); 1527 1528 mThemeCombo.add(THEME_SEPARATOR); 1529 mIsProjectTheme.add(Boolean.FALSE); 1530 } 1531 1532 // Create a sorted list of unique themes referenced in the manifest 1533 // (sort alphabetically, but place the preferred theme at the 1534 // top of the list) 1535 Set<String> themeSet = new HashSet<String>(activityThemes.values()); 1536 themeSet.add(defaultTheme); 1537 List<String> themeList = new ArrayList<String>(themeSet); 1538 final String first = preferred; 1539 Collections.sort(themeList, new Comparator<String>() { 1540 @Override 1541 public int compare(String s1, String s2) { 1542 if (s1 == first) { 1543 return -1; 1544 } else if (s1 == first) { 1545 return 1; 1546 } else { 1547 return s1.compareTo(s2); 1548 } 1549 } 1550 }); 1551 1552 if (themeList.size() > 1 || 1553 (themeList.size() == 1 && (preferred == null || 1554 !preferred.equals(themeList.get(0))))) { 1555 for (String style : themeList) { 1556 String theme = ResourceHelper.styleToTheme(style); 1557 1558 // Initialize the chosen theme to the first item 1559 // in the used theme list (that's what would be chosen 1560 // anyway) such that we stop attempting to look up 1561 // the associated activity (during initialization, 1562 // this method can be called repeatedly.) 1563 if (mState.theme == null) { 1564 mState.theme = theme; 1565 } 1566 1567 boolean isProjectTheme = !style.startsWith(PREFIX_ANDROID_STYLE); 1568 mThemeCombo.add(theme); 1569 mIsProjectTheme.add(Boolean.valueOf(isProjectTheme)); 1570 } 1571 mThemeCombo.add(THEME_SEPARATOR); 1572 mIsProjectTheme.add(Boolean.FALSE); 1573 } 1574 } 1575 1576 // now get the themes and languages from the project. 1577 int projectThemeCount = 0; 1578 ResourceRepository projectRes = mListener.getProjectResources(); 1579 // in cases where the opened file is not linked to a project, this could be null. 1580 if (projectRes != null) { 1581 // get the configured resources for the project 1582 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = 1583 mListener.getConfiguredProjectResources(); 1584 1585 if (configuredProjectRes != null) { 1586 // get the styles. 1587 Map<String, ResourceValue> styleMap = configuredProjectRes.get( 1588 ResourceType.STYLE); 1589 1590 if (styleMap != null) { 1591 // collect the themes out of all the styles, ie styles that extend, 1592 // directly or indirectly a platform theme. 1593 for (ResourceValue value : styleMap.values()) { 1594 if (isTheme(value, styleMap, null)) { 1595 themes.add(value.getName()); 1596 } 1597 } 1598 1599 Collections.sort(themes); 1600 1601 for (String theme : themes) { 1602 mThemeCombo.add(theme); 1603 mIsProjectTheme.add(Boolean.TRUE); 1604 } 1605 } 1606 } 1607 projectThemeCount = themes.size(); 1608 themes.clear(); 1609 } 1610 1611 // get the themes, and languages from the Framework. 1612 if (frameworkRes != null) { 1613 // get the configured resources for the framework 1614 Map<ResourceType, Map<String, ResourceValue>> frameworResources = 1615 frameworkRes.getConfiguredResources(getCurrentConfig()); 1616 1617 if (frameworResources != null) { 1618 // get the styles. 1619 Map<String, ResourceValue> styles = frameworResources.get(ResourceType.STYLE); 1620 1621 1622 // collect the themes out of all the styles. 1623 for (ResourceValue value : styles.values()) { 1624 String name = value.getName(); 1625 if (name.startsWith("Theme.") || name.equals("Theme")) { 1626 themes.add(value.getName()); 1627 } 1628 } 1629 1630 // sort them and add them to the combo 1631 Collections.sort(themes); 1632 1633 if (projectThemeCount > 0 && themes.size() > 0) { 1634 mThemeCombo.add(THEME_SEPARATOR); 1635 mIsProjectTheme.add(Boolean.FALSE); 1636 } 1637 1638 for (String theme : themes) { 1639 mThemeCombo.add(theme); 1640 mIsProjectTheme.add(Boolean.FALSE); 1641 } 1642 1643 themes.clear(); 1644 } 1645 } 1646 1647 // try to reselect the previous theme. 1648 boolean needDefaultSelection = true; 1649 1650 if (mState.theme != null && includedIn == null) { 1651 final int count = mThemeCombo.getItemCount(); 1652 for (int i = 0 ; i < count ; i++) { 1653 if (mState.theme.equals(mThemeCombo.getItem(i))) { 1654 mThemeCombo.select(i); 1655 needDefaultSelection = false; 1656 mThemeCombo.setEnabled(true); 1657 break; 1658 } 1659 } 1660 } 1661 1662 if (needDefaultSelection) { 1663 if (mThemeCombo.getItemCount() > 0) { 1664 mThemeCombo.select(0); 1665 mThemeCombo.setEnabled(true); 1666 } else { 1667 mThemeCombo.setEnabled(false); 1668 } 1669 } 1670 1671 mThemeCombo.getParent().layout(); 1672 } finally { 1673 mDisableUpdates--; 1674 } 1675 1676 assert mIsProjectTheme.size() == mThemeCombo.getItemCount(); 1677 } 1678 1679 // ---- getters for the config selection values ---- 1680 1681 public FolderConfiguration getEditedConfig() { 1682 return mEditedConfig; 1683 } 1684 1685 public FolderConfiguration getCurrentConfig() { 1686 return mCurrentConfig; 1687 } 1688 1689 public void getCurrentConfig(FolderConfiguration config) { 1690 config.set(mCurrentConfig); 1691 } 1692 1693 /** 1694 * Returns the currently selected {@link Density}. This is guaranteed to be non null. 1695 */ 1696 public Density getDensity() { 1697 if (mCurrentConfig != null) { 1698 DensityQualifier qual = mCurrentConfig.getDensityQualifier(); 1699 if (qual != null) { 1700 // just a sanity check 1701 Density d = qual.getValue(); 1702 if (d != Density.NODPI) { 1703 return d; 1704 } 1705 } 1706 } 1707 1708 // no config? return medium as the default density. 1709 return Density.MEDIUM; 1710 } 1711 1712 /** 1713 * Returns the current device xdpi. 1714 */ 1715 public float getXDpi() { 1716 if (mState.device != null) { 1717 float dpi = mState.device.getXDpi(); 1718 if (Float.isNaN(dpi) == false) { 1719 return dpi; 1720 } 1721 } 1722 1723 // get the pixel density as the density. 1724 return getDensity().getDpiValue(); 1725 } 1726 1727 /** 1728 * Returns the current device ydpi. 1729 */ 1730 public float getYDpi() { 1731 if (mState.device != null) { 1732 float dpi = mState.device.getYDpi(); 1733 if (Float.isNaN(dpi) == false) { 1734 return dpi; 1735 } 1736 } 1737 1738 // get the pixel density as the density. 1739 return getDensity().getDpiValue(); 1740 } 1741 1742 public Rect getScreenBounds() { 1743 // get the orientation from the current device config 1744 ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier(); 1745 ScreenOrientation orientation = ScreenOrientation.PORTRAIT; 1746 if (qual != null) { 1747 orientation = qual.getValue(); 1748 } 1749 1750 // get the device screen dimension 1751 ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier(); 1752 int s1, s2; 1753 if (qual2 != null) { 1754 s1 = qual2.getValue1(); 1755 s2 = qual2.getValue2(); 1756 } else { 1757 s1 = 480; 1758 s2 = 320; 1759 } 1760 1761 switch (orientation) { 1762 default: 1763 case PORTRAIT: 1764 return new Rect(0, 0, s2, s1); 1765 case LANDSCAPE: 1766 return new Rect(0, 0, s1, s2); 1767 case SQUARE: 1768 return new Rect(0, 0, s1, s1); 1769 } 1770 } 1771 1772 /** 1773 * Returns the current theme, or null if the combo has no selection. 1774 * 1775 * @return the theme name, or null 1776 */ 1777 public String getTheme() { 1778 int themeIndex = mThemeCombo.getSelectionIndex(); 1779 if (themeIndex != -1) { 1780 return mThemeCombo.getItem(themeIndex); 1781 } 1782 1783 return null; 1784 } 1785 1786 /** 1787 * Returns the current device string, or null if the combo has no selection. 1788 * 1789 * @return the device name, or null 1790 */ 1791 public String getDevice() { 1792 int deviceIndex = mDeviceCombo.getSelectionIndex(); 1793 if (deviceIndex != -1) { 1794 return mDeviceCombo.getItem(deviceIndex); 1795 } 1796 1797 return null; 1798 } 1799 1800 /** 1801 * Returns whether the current theme selection is a project theme. 1802 * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>. 1803 * @return true for project theme, false for framework theme 1804 */ 1805 public boolean isProjectTheme() { 1806 return mIsProjectTheme.get(mThemeCombo.getSelectionIndex()).booleanValue(); 1807 } 1808 1809 public IAndroidTarget getRenderingTarget() { 1810 int index = mTargetCombo.getSelectionIndex(); 1811 if (index >= 0) { 1812 return mTargetList.get(index); 1813 } 1814 1815 return null; 1816 } 1817 1818 /** 1819 * Loads the list of {@link IAndroidTarget} and inits the UI with it. 1820 */ 1821 private void initTargets() { 1822 mTargetCombo.removeAll(); 1823 mTargetList.clear(); 1824 1825 Sdk currentSdk = Sdk.getCurrent(); 1826 if (currentSdk != null) { 1827 IAndroidTarget[] targets = currentSdk.getTargets(); 1828 int match = -1; 1829 for (int i = 0 ; i < targets.length; i++) { 1830 // FIXME: add check based on project minSdkVersion 1831 if (targets[i].hasRenderingLibrary()) { 1832 mTargetCombo.add(targets[i].getShortClasspathName()); 1833 mTargetList.add(targets[i]); 1834 1835 if (mRenderingTarget != null) { 1836 // use equals because the rendering could be from a previous SDK, so 1837 // it may not be the same instance. 1838 if (mRenderingTarget.equals(targets[i])) { 1839 match = mTargetList.indexOf(targets[i]); 1840 } 1841 } else if (mProjectTarget == targets[i]) { 1842 match = mTargetList.indexOf(targets[i]); 1843 } 1844 } 1845 } 1846 1847 mTargetCombo.setEnabled(mTargetList.size() > 1); 1848 if (match == -1) { 1849 mTargetCombo.deselectAll(); 1850 1851 // the rendering target is the same as the project. 1852 mRenderingTarget = mProjectTarget; 1853 } else { 1854 mTargetCombo.select(match); 1855 1856 // set the rendering target to the new object. 1857 mRenderingTarget = mTargetList.get(match); 1858 } 1859 } 1860 } 1861 1862 /** 1863 * Loads the list of {@link LayoutDevice} and inits the UI with it. 1864 */ 1865 private void initDevices() { 1866 mDeviceList = null; 1867 1868 Sdk sdk = Sdk.getCurrent(); 1869 if (sdk != null) { 1870 LayoutDeviceManager manager = sdk.getLayoutDeviceManager(); 1871 mDeviceList = manager.getCombinedList(); 1872 } 1873 1874 1875 // remove older devices if applicable 1876 mDeviceCombo.removeAll(); 1877 mDeviceConfigCombo.removeAll(); 1878 1879 // fill with the devices 1880 if (mDeviceList != null) { 1881 for (LayoutDevice device : mDeviceList) { 1882 mDeviceCombo.add(device.getName()); 1883 } 1884 mDeviceCombo.select(0); 1885 1886 if (mDeviceList.size() > 0) { 1887 List<DeviceConfig> configs = mDeviceList.get(0).getConfigs(); 1888 for (DeviceConfig config : configs) { 1889 mDeviceConfigCombo.add(config.getName()); 1890 } 1891 mDeviceConfigCombo.select(0); 1892 if (configs.size() == 1) { 1893 mDeviceConfigCombo.setEnabled(false); 1894 } 1895 } 1896 } 1897 1898 // add the custom item 1899 mDeviceCombo.add("Custom..."); 1900 } 1901 1902 /** 1903 * Selects a given {@link LayoutDevice} in the device combo, if it is found. 1904 * @param device the device to select 1905 * @return true if the device was found. 1906 */ 1907 private boolean selectDevice(LayoutDevice device) { 1908 final int count = mDeviceList.size(); 1909 for (int i = 0 ; i < count ; i++) { 1910 // since device comes from mDeviceList, we can use the == operator. 1911 if (device == mDeviceList.get(i)) { 1912 mDeviceCombo.select(i); 1913 return true; 1914 } 1915 } 1916 1917 return false; 1918 } 1919 1920 /** 1921 * Selects a config by name. 1922 * @param name the name of the config to select. 1923 */ 1924 private void selectConfig(String name) { 1925 final int count = mDeviceConfigCombo.getItemCount(); 1926 for (int i = 0 ; i < count ; i++) { 1927 String item = mDeviceConfigCombo.getItem(i); 1928 if (name.equals(item)) { 1929 mDeviceConfigCombo.select(i); 1930 return; 1931 } 1932 } 1933 } 1934 1935 /** 1936 * Called when the selection of the device combo changes. 1937 * @param recomputeLayout 1938 */ 1939 private void onDeviceChange(boolean recomputeLayout) { 1940 // because changing the content of a combo triggers a change event, respect the 1941 // mDisableUpdates flag 1942 if (mDisableUpdates > 0) { 1943 return; 1944 } 1945 1946 String newConfigName = null; 1947 1948 int deviceIndex = mDeviceCombo.getSelectionIndex(); 1949 if (deviceIndex != -1) { 1950 // check if the user is asking for the custom item 1951 if (deviceIndex == mDeviceCombo.getItemCount() - 1) { 1952 onCustomDeviceConfig(); 1953 return; 1954 } 1955 1956 // get the previous config, so that we can look for a close match 1957 if (mState.device != null) { 1958 int index = mDeviceConfigCombo.getSelectionIndex(); 1959 if (index != -1) { 1960 FolderConfiguration oldConfig = mState.device.getFolderConfigByName( 1961 mDeviceConfigCombo.getItem(index)); 1962 1963 LayoutDevice newDevice = mDeviceList.get(deviceIndex); 1964 1965 newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs()); 1966 } 1967 } 1968 1969 mState.device = mDeviceList.get(deviceIndex); 1970 } else { 1971 mState.device = null; 1972 } 1973 1974 fillConfigCombo(newConfigName); 1975 1976 computeCurrentConfig(); 1977 1978 if (recomputeLayout) { 1979 onDeviceConfigChange(); 1980 } 1981 } 1982 1983 /** 1984 * Handles a user request for the {@link ConfigManagerDialog}. 1985 */ 1986 private void onCustomDeviceConfig() { 1987 ConfigManagerDialog dialog = new ConfigManagerDialog(getShell()); 1988 dialog.open(); 1989 1990 // save the user devices 1991 Sdk.getCurrent().getLayoutDeviceManager().save(); 1992 1993 // Update the UI with no triggered event 1994 mDisableUpdates++; 1995 1996 try { 1997 LayoutDevice oldCurrent = mState.device; 1998 1999 // but first, update the device combo 2000 initDevices(); 2001 2002 // attempts to reselect the current device. 2003 if (selectDevice(oldCurrent)) { 2004 // current device still exists. 2005 // reselect the config 2006 selectConfig(mState.configName); 2007 2008 // reset the UI as if it was just a replacement file, since we can keep 2009 // the current device (and possibly config). 2010 adaptConfigSelection(false /*needBestMatch*/); 2011 2012 } else { 2013 // find a new device/config to match the current file. 2014 findAndSetCompatibleConfig(false /*favorCurrentConfig*/); 2015 } 2016 } finally { 2017 mDisableUpdates--; 2018 } 2019 2020 // recompute the current config 2021 computeCurrentConfig(); 2022 2023 // force a redraw 2024 onDeviceChange(true /*recomputeLayout*/); 2025 } 2026 2027 /** 2028 * Attempts to find a close config among a list 2029 * @param oldConfig the reference config. 2030 * @param configs the list of config to search through 2031 * @return the name of the closest config match, or possibly null if no configs are compatible 2032 * (this can only happen if the configs don't have a single qualifier that is the same). 2033 */ 2034 private String getClosestMatch(FolderConfiguration oldConfig, List<DeviceConfig> configs) { 2035 2036 // create 2 lists as we're going to go through one and put the candidates in the other. 2037 ArrayList<DeviceConfig> list1 = new ArrayList<DeviceConfig>(); 2038 ArrayList<DeviceConfig> list2 = new ArrayList<DeviceConfig>(); 2039 2040 list1.addAll(configs); 2041 2042 final int count = FolderConfiguration.getQualifierCount(); 2043 for (int i = 0 ; i < count ; i++) { 2044 // compute the new candidate list by only taking configs that have 2045 // the same i-th qualifier as the old config 2046 for (DeviceConfig c : list1) { 2047 ResourceQualifier oldQualifier = oldConfig.getQualifier(i); 2048 2049 FolderConfiguration folderConfig = c.getConfig(); 2050 ResourceQualifier newQualifier = folderConfig.getQualifier(i); 2051 2052 if (oldQualifier == null) { 2053 if (newQualifier == null) { 2054 list2.add(c); 2055 } 2056 } else if (oldQualifier.equals(newQualifier)) { 2057 list2.add(c); 2058 } 2059 } 2060 2061 // at any moment if the new candidate list contains only one match, its name 2062 // is returned. 2063 if (list2.size() == 1) { 2064 return list2.get(0).getName(); 2065 } 2066 2067 // if the list is empty, then all the new configs failed. It is considered ok, and 2068 // we move to the next qualifier anyway. This way, if a qualifier is different for 2069 // all new configs it is simply ignored. 2070 if (list2.size() != 0) { 2071 // move the candidates back into list1. 2072 list1.clear(); 2073 list1.addAll(list2); 2074 list2.clear(); 2075 } 2076 } 2077 2078 // the only way to reach this point is if there's an exact match. 2079 // (if there are more than one, then there's a duplicate config and it doesn't matter, 2080 // we take the first one). 2081 if (list1.size() > 0) { 2082 return list1.get(0).getName(); 2083 } 2084 2085 return null; 2086 } 2087 2088 /** 2089 * fills the config combo with new values based on {@link #mState}.device. 2090 * @param refName an optional name. if set the selection will match this name (if found) 2091 */ 2092 private void fillConfigCombo(String refName) { 2093 mDeviceConfigCombo.removeAll(); 2094 2095 if (mState.device != null) { 2096 int selectionIndex = 0; 2097 int i = 0; 2098 2099 for (DeviceConfig config : mState.device.getConfigs()) { 2100 mDeviceConfigCombo.add(config.getName()); 2101 2102 if (config.getName().equals(refName)) { 2103 selectionIndex = i; 2104 } 2105 i++; 2106 } 2107 2108 mDeviceConfigCombo.select(selectionIndex); 2109 mDeviceConfigCombo.setEnabled(mState.device.getConfigs().size() > 1); 2110 } 2111 } 2112 2113 /** 2114 * Called when the device config selection changes. 2115 */ 2116 private void onDeviceConfigChange() { 2117 // because changing the content of a combo triggers a change event, respect the 2118 // mDisableUpdates flag 2119 if (mDisableUpdates > 0) { 2120 return; 2121 } 2122 2123 if (computeCurrentConfig() && mListener != null) { 2124 mListener.onConfigurationChange(); 2125 mListener.onDevicePostChange(); 2126 } 2127 } 2128 2129 /** 2130 * Call back for language combo selection 2131 */ 2132 private void onLocaleChange() { 2133 // because mLocaleList triggers onLocaleChange at each modification, the filling 2134 // of the combo with data will trigger notifications, and we don't want that. 2135 if (mDisableUpdates > 0) { 2136 return; 2137 } 2138 2139 if (computeCurrentConfig() && mListener != null) { 2140 mListener.onConfigurationChange(); 2141 } 2142 2143 // Store locale project-wide setting 2144 saveRenderState(); 2145 } 2146 2147 private void onDockChange() { 2148 if (computeCurrentConfig() && mListener != null) { 2149 mListener.onConfigurationChange(); 2150 } 2151 } 2152 2153 private void onDayChange() { 2154 if (computeCurrentConfig() && mListener != null) { 2155 mListener.onConfigurationChange(); 2156 } 2157 } 2158 2159 /** 2160 * Call back for api level combo selection 2161 */ 2162 private void onRenderingTargetChange() { 2163 // because mApiCombo triggers onApiLevelChange at each modification, the filling 2164 // of the combo with data will trigger notifications, and we don't want that. 2165 if (mDisableUpdates > 0) { 2166 return; 2167 } 2168 2169 // tell the listener a new rendering target is being set. Need to do this before updating 2170 // mRenderingTarget. 2171 if (mListener != null && mRenderingTarget != null) { 2172 mListener.onRenderingTargetPreChange(mRenderingTarget); 2173 } 2174 2175 int index = mTargetCombo.getSelectionIndex(); 2176 mRenderingTarget = mTargetList.get(index); 2177 2178 boolean computeOk = computeCurrentConfig(); 2179 2180 // force a theme update to reflect the new rendering target. 2181 // This must be done after computeCurrentConfig since it'll depend on the currentConfig 2182 // to figure out the theme list. 2183 updateThemes(); 2184 2185 // since the state is saved in computeCurrentConfig, we need to resave it since theme 2186 // change could have impacted it. 2187 saveState(); 2188 2189 if (mListener != null && mRenderingTarget != null) { 2190 mListener.onRenderingTargetPostChange(mRenderingTarget); 2191 } 2192 2193 // Store project-wide render-target setting 2194 saveRenderState(); 2195 2196 if (computeOk && mListener != null) { 2197 mListener.onConfigurationChange(); 2198 } 2199 } 2200 2201 /** 2202 * Saves the current state and the current configuration 2203 * 2204 * @see #saveState() 2205 */ 2206 private boolean computeCurrentConfig() { 2207 saveState(); 2208 2209 if (mState.device != null) { 2210 // get the device config from the device/config combos. 2211 int configIndex = mDeviceConfigCombo.getSelectionIndex(); 2212 String name = mDeviceConfigCombo.getItem(configIndex); 2213 FolderConfiguration config = mState.device.getFolderConfigByName(name); 2214 2215 // replace the config with the one from the device 2216 mCurrentConfig.set(config); 2217 2218 // replace the locale qualifiers with the one coming from the locale combo 2219 int index = mLocaleCombo.getSelectionIndex(); 2220 if (index != -1) { 2221 ResourceQualifier[] localeQualifiers = mLocaleList.get(index); 2222 2223 mCurrentConfig.setLanguageQualifier( 2224 (LanguageQualifier)localeQualifiers[LOCALE_LANG]); 2225 mCurrentConfig.setRegionQualifier( 2226 (RegionQualifier)localeQualifiers[LOCALE_REGION]); 2227 } 2228 2229 index = mUiModeCombo.getSelectionIndex(); 2230 if (index == -1) { 2231 index = 0; // no selection = 0 2232 } 2233 mCurrentConfig.setUiModeQualifier(new UiModeQualifier(UiMode.getByIndex(index))); 2234 2235 index = mNightCombo.getSelectionIndex(); 2236 if (index == -1) { 2237 index = 0; // no selection = 0 2238 } 2239 mCurrentConfig.setNightModeQualifier( 2240 new NightModeQualifier(NightMode.getByIndex(index))); 2241 2242 // replace the API level by the selection of the combo 2243 index = mTargetCombo.getSelectionIndex(); 2244 if (index == -1) { 2245 index = mTargetList.indexOf(mProjectTarget); 2246 } 2247 if (index != -1) { 2248 IAndroidTarget target = mTargetList.get(index); 2249 2250 if (target != null) { 2251 mCurrentConfig.setVersionQualifier( 2252 new VersionQualifier(target.getVersion().getApiLevel())); 2253 } 2254 } 2255 2256 // update the create button. 2257 checkCreateEnable(); 2258 2259 return true; 2260 } 2261 2262 return false; 2263 } 2264 2265 private void onThemeChange() { 2266 saveState(); 2267 2268 int themeIndex = mThemeCombo.getSelectionIndex(); 2269 if (themeIndex != -1) { 2270 String theme = mThemeCombo.getItem(themeIndex); 2271 2272 if (theme.equals(THEME_SEPARATOR)) { 2273 mThemeCombo.select(0); 2274 } 2275 2276 if (mListener != null) { 2277 mListener.onThemeChange(); 2278 } 2279 } 2280 } 2281 2282 /** 2283 * Returns whether the given <var>style</var> is a theme. 2284 * This is done by making sure the parent is a theme. 2285 * @param value the style to check 2286 * @param styleMap the map of styles for the current project. Key is the style name. 2287 * @param seen the map of styles we have already processed (or null if not yet 2288 * initialized). Only the keys are significant (since there is no IdentityHashSet). 2289 * @return True if the given <var>style</var> is a theme. 2290 */ 2291 private boolean isTheme(ResourceValue value, Map<String, ResourceValue> styleMap, 2292 IdentityHashMap<ResourceValue, Boolean> seen) { 2293 if (value instanceof StyleResourceValue) { 2294 StyleResourceValue style = (StyleResourceValue)value; 2295 2296 boolean frameworkStyle = false; 2297 String parentStyle = style.getParentStyle(); 2298 if (parentStyle == null) { 2299 // if there is no specified parent style we look an implied one. 2300 // For instance 'Theme.light' is implied child style of 'Theme', 2301 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light' 2302 String name = style.getName(); 2303 int index = name.lastIndexOf('.'); 2304 if (index != -1) { 2305 parentStyle = name.substring(0, index); 2306 } 2307 } else { 2308 // remove the useless @ if it's there 2309 if (parentStyle.startsWith("@")) { 2310 parentStyle = parentStyle.substring(1); 2311 } 2312 2313 // check for framework identifier. 2314 if (parentStyle.startsWith(ANDROID_NS_NAME_PREFIX)) { 2315 frameworkStyle = true; 2316 parentStyle = parentStyle.substring(ANDROID_NS_NAME_PREFIX.length()); 2317 } 2318 2319 // at this point we could have the format style/<name>. we want only the name 2320 if (parentStyle.startsWith("style/")) { 2321 parentStyle = parentStyle.substring("style/".length()); 2322 } 2323 } 2324 2325 if (parentStyle != null) { 2326 if (frameworkStyle) { 2327 // if the parent is a framework style, it has to be 'Theme' or 'Theme.*' 2328 return parentStyle.equals("Theme") || parentStyle.startsWith("Theme."); 2329 } else { 2330 // if it's a project style, we check this is a theme. 2331 ResourceValue parentValue = styleMap.get(parentStyle); 2332 2333 // also prevent stack overflow in case the dev mistakenly declared 2334 // the parent of the style as the style itself. 2335 if (parentValue != null && parentValue.equals(value) == false) { 2336 if (seen == null) { 2337 seen = new IdentityHashMap<ResourceValue, Boolean>(); 2338 seen.put(value, Boolean.TRUE); 2339 } else if (seen.containsKey(parentValue)) { 2340 return false; 2341 } 2342 seen.put(parentValue, Boolean.TRUE); 2343 return isTheme(parentValue, styleMap, seen); 2344 } 2345 } 2346 } 2347 } 2348 2349 return false; 2350 } 2351 2352 private void checkCreateEnable() { 2353 mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false); 2354 } 2355 2356 /** 2357 * Checks whether the current edited file is the best match for a given config. 2358 * <p/> 2359 * This tests against other versions of the same layout in the project. 2360 * <p/> 2361 * The given config must be compatible with the current edited file. 2362 * @param config the config to test. 2363 * @return true if the current edited file is the best match in the project for the 2364 * given config. 2365 */ 2366 private boolean isCurrentFileBestMatchFor(FolderConfiguration config) { 2367 ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), 2368 ResourceFolderType.LAYOUT, config); 2369 2370 if (match != null) { 2371 return match.getFile().equals(mEditedFile); 2372 } else { 2373 // if we stop here that means the current file is not even a match! 2374 AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config."); 2375 } 2376 2377 return false; 2378 } 2379 2380 /** 2381 * Resets the configuration chooser to reflect the given file configuration. This is 2382 * intended to be used by the "Show Included In" functionality where the user has 2383 * picked a non-default configuration (such as a particular landscape layout) and the 2384 * configuration chooser must be switched to a landscape layout. This method will 2385 * trigger a model change. 2386 * <p> 2387 * This will NOT trigger a redraw event! 2388 * <p> 2389 * FIXME: We are currently setting the configuration file to be the configuration for 2390 * the "outer" (the including) file, rather than the inner file, which is the file the 2391 * user is actually editing. We need to refine this, possibly with a way for the user 2392 * to choose which configuration they are editing. And in particular, we should be 2393 * filtering the configuration chooser to only show options in the outer configuration 2394 * that are compatible with the inner included file. 2395 * 2396 * @param file the file to be configured 2397 */ 2398 public void resetConfigFor(IFile file) { 2399 setFile(file); 2400 mEditedConfig = null; 2401 onXmlModelLoaded(); 2402 } 2403 2404 /** 2405 * Syncs this configuration to the project wide locale and render target settings. The 2406 * locale may ignore the project-wide setting if it is a locale-specific 2407 * configuration. 2408 * 2409 * @return true if one or both of the toggles were changed, false if there were no 2410 * changes 2411 */ 2412 public boolean syncRenderState() { 2413 if (mEditedConfig == null) { 2414 // Startup; ignore 2415 return false; 2416 } 2417 2418 boolean localeChanged = false; 2419 boolean renderTargetChanged = false; 2420 2421 // When a page is re-activated, force the toggles to reflect the current project 2422 // state 2423 2424 Pair<ResourceQualifier[], IAndroidTarget> pair = loadRenderState(); 2425 2426 // Only sync the locale if this layout is not already a locale-specific layout! 2427 if (!isLocaleSpecificLayout()) { 2428 ResourceQualifier[] locale = pair.getFirst(); 2429 if (locale != null) { 2430 localeChanged = setLocaleCombo(locale[0], locale[1]); 2431 } 2432 } 2433 2434 // Sync render target 2435 IAndroidTarget target = pair.getSecond(); 2436 if (target != null) { 2437 int targetIndex = mTargetList.indexOf(target); 2438 if (targetIndex != mTargetCombo.getSelectionIndex()) { 2439 mTargetCombo.select(targetIndex); 2440 renderTargetChanged = true; 2441 } 2442 } 2443 2444 if (!renderTargetChanged && !localeChanged) { 2445 return false; 2446 } 2447 2448 // Update the locale and/or the render target. This code contains a logical 2449 // merge of the onRenderingTargetChange() and onLocaleChange() methods, combined 2450 // such that we don't duplicate work. 2451 2452 if (renderTargetChanged) { 2453 if (mListener != null && mRenderingTarget != null) { 2454 mListener.onRenderingTargetPreChange(mRenderingTarget); 2455 } 2456 int targetIndex = mTargetCombo.getSelectionIndex(); 2457 mRenderingTarget = mTargetList.get(targetIndex); 2458 } 2459 2460 // Compute the new configuration; we want to do this both for locale changes 2461 // and for render targets. 2462 boolean computeOk = computeCurrentConfig(); 2463 2464 if (renderTargetChanged) { 2465 // force a theme update to reflect the new rendering target. 2466 // This must be done after computeCurrentConfig since it'll depend on the currentConfig 2467 // to figure out the theme list. 2468 updateThemes(); 2469 2470 if (mListener != null && mRenderingTarget != null) { 2471 mListener.onRenderingTargetPostChange(mRenderingTarget); 2472 } 2473 } 2474 2475 // For both locale and render target changes 2476 if (computeOk && mListener != null) { 2477 mListener.onConfigurationChange(); 2478 } 2479 2480 return true; 2481 } 2482 2483 /** 2484 * Loads the render state (the locale and the render target, which are shared among 2485 * all the layouts meaning that changing it in one will change it in all) and returns 2486 * the current project-wide locale and render target to be used. 2487 * 2488 * @return a pair of locale resource qualifiers and render target 2489 */ 2490 private Pair<ResourceQualifier[], IAndroidTarget> loadRenderState() { 2491 IProject project = mEditedFile.getProject(); 2492 try { 2493 String data = project.getPersistentProperty(NAME_RENDER_STATE); 2494 if (data != null) { 2495 ResourceQualifier[] locale = null; 2496 IAndroidTarget target = null; 2497 2498 String[] values = data.split(SEP); 2499 if (values.length == 2) { 2500 locale = new ResourceQualifier[2]; 2501 String locales[] = values[0].split(SEP_LOCALE); 2502 if (locales.length >= 2) { 2503 if (locales[0].length() > 0) { 2504 locale[0] = new LanguageQualifier(locales[0]); 2505 } 2506 if (locales[1].length() > 0) { 2507 locale[1] = new RegionQualifier(locales[1]); 2508 } 2509 } 2510 target = stringToTarget(values[1]); 2511 2512 // See if we should "correct" the rendering target to a better version. 2513 // If you're using a pre-release version of the render target, and a 2514 // final release is available and installed, we should switch to that 2515 // one instead. 2516 if (target != null) { 2517 AndroidVersion version = target.getVersion(); 2518 if (version.getCodename() != null && mTargetList != null) { 2519 int targetApiLevel = version.getApiLevel() + 1; 2520 for (IAndroidTarget t : mTargetList) { 2521 if (t.getVersion().getApiLevel() == targetApiLevel 2522 && t.isPlatform()) { 2523 target = t; 2524 break; 2525 } 2526 } 2527 } 2528 } 2529 } 2530 2531 return Pair.of(locale, target); 2532 } 2533 2534 ResourceQualifier[] any = new ResourceQualifier[] { 2535 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), 2536 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) 2537 }; 2538 2539 return Pair.of(any, findDefaultRenderTarget()); 2540 } catch (CoreException e) { 2541 AdtPlugin.log(e, null); 2542 } 2543 2544 return null; 2545 } 2546 2547 /** Returns true if the current layout is locale-specific */ 2548 private boolean isLocaleSpecificLayout() { 2549 return mEditedConfig == null || mEditedConfig.getLanguageQualifier() != null; 2550 } 2551 2552 /** 2553 * Saves the render state (the current locale and render target settings) into the 2554 * project wide settings storage 2555 */ 2556 private void saveRenderState() { 2557 IProject project = mEditedFile.getProject(); 2558 try { 2559 int index = mLocaleCombo.getSelectionIndex(); 2560 ResourceQualifier[] locale = mLocaleList.get(index); 2561 index = mTargetCombo.getSelectionIndex(); 2562 IAndroidTarget target = mTargetList.get(index); 2563 2564 // Generate a persistent string from locale+target 2565 StringBuilder sb = new StringBuilder(); 2566 if (locale != null) { 2567 if (locale[0] != null && locale[1] != null) { 2568 // locale[0]/[1] can be null sometimes when starting Eclipse 2569 sb.append(((LanguageQualifier) locale[0]).getValue()); 2570 sb.append(SEP_LOCALE); 2571 sb.append(((RegionQualifier) locale[1]).getValue()); 2572 } 2573 } 2574 sb.append(SEP); 2575 if (target != null) { 2576 sb.append(targetToString(target)); 2577 sb.append(SEP); 2578 } 2579 2580 String data = sb.toString(); 2581 project.setPersistentProperty(NAME_RENDER_STATE, data); 2582 } catch (CoreException e) { 2583 AdtPlugin.log(e, null); 2584 } 2585 } 2586 } 2587