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