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 com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 21 import com.android.ide.eclipse.adt.internal.resources.ResourceType; 22 import com.android.ide.eclipse.adt.internal.resources.configurations.DockModeQualifier; 23 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration; 24 import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier; 25 import com.android.ide.eclipse.adt.internal.resources.configurations.NightModeQualifier; 26 import com.android.ide.eclipse.adt.internal.resources.configurations.PixelDensityQualifier; 27 import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier; 28 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier; 29 import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenDimensionQualifier; 30 import com.android.ide.eclipse.adt.internal.resources.configurations.ScreenOrientationQualifier; 31 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 32 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile; 33 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder; 34 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; 35 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 36 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 37 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice; 38 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager; 39 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; 40 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 41 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge; 42 import com.android.layoutlib.api.IResourceValue; 43 import com.android.layoutlib.api.IStyleResourceValue; 44 import com.android.sdklib.IAndroidTarget; 45 import com.android.sdklib.resources.Density; 46 import com.android.sdklib.resources.DockMode; 47 import com.android.sdklib.resources.NightMode; 48 import com.android.sdklib.resources.ScreenOrientation; 49 50 import org.eclipse.core.resources.IFile; 51 import org.eclipse.core.resources.IFolder; 52 import org.eclipse.core.resources.IProject; 53 import org.eclipse.core.runtime.CoreException; 54 import org.eclipse.core.runtime.IStatus; 55 import org.eclipse.core.runtime.QualifiedName; 56 import org.eclipse.draw2d.geometry.Rectangle; 57 import org.eclipse.swt.SWT; 58 import org.eclipse.swt.events.SelectionAdapter; 59 import org.eclipse.swt.events.SelectionEvent; 60 import org.eclipse.swt.events.SelectionListener; 61 import org.eclipse.swt.graphics.Image; 62 import org.eclipse.swt.layout.GridData; 63 import org.eclipse.swt.layout.GridLayout; 64 import org.eclipse.swt.widgets.Button; 65 import org.eclipse.swt.widgets.Combo; 66 import org.eclipse.swt.widgets.Composite; 67 import org.eclipse.swt.widgets.Label; 68 69 import java.util.ArrayList; 70 import java.util.Collections; 71 import java.util.List; 72 import java.util.Map; 73 import java.util.Set; 74 import java.util.SortedSet; 75 import java.util.Map.Entry; 76 77 /** 78 * A composite that displays the current configuration displayed in a Graphical Layout Editor. 79 * <p/> 80 * The composite has several entry points:<br> 81 * - {@link #setFile(File)}<br> 82 * Called after the constructor to set the file being edited. Nothing else is performed.<br> 83 *<br> 84 * - {@link #onXmlModelLoaded()}<br> 85 * Called when the XML model is loaded, either the first time or when the Target/SDK changes. 86 * This initializes the UI, either with the first compatible configuration found, or attempts 87 * to restore a configuration if one is found to have been saved in the file persistent storage. 88 * (see {@link #storeState()})<br> 89 *<br> 90 * - {@link #replaceFile(File)}<br> 91 * Called when a file, representing the same resource but with a different config is opened<br> 92 * by the user.<br> 93 *<br> 94 * - {@link #changeFileOnNewConfig(FolderConfiguration)}<br> 95 * Called when config change triggers the editing of a file with a different config. 96 *<p/> 97 * Additionally, the composite can handle the following events.<br> 98 * - SDK reload. This is when the main SDK is finished loading.<br> 99 * - Target reload. This is when the target used by the project is the edited file has finished<br> 100 * loading.<br> 101 */ 102 public class ConfigurationComposite extends Composite { 103 104 private final static String CONFIG_STATE = "state"; //$NON-NLS-1$ 105 private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$ 106 107 private final static int LOCALE_LANG = 0; 108 private final static int LOCALE_REGION = 1; 109 110 private Button mClippingButton; 111 private Label mCurrentLayoutLabel; 112 113 private Combo mDeviceCombo; 114 private Combo mDeviceConfigCombo; 115 private Combo mLocaleCombo; 116 private Combo mDockCombo; 117 private Combo mNightCombo; 118 private Combo mThemeCombo; 119 private Button mCreateButton; 120 121 private int mPlatformThemeCount = 0; 122 /** updates are disabled if > 0 */ 123 private int mDisableUpdates = 0; 124 125 private List<LayoutDevice> mDeviceList; 126 127 private final ArrayList<ResourceQualifier[] > mLocaleList = 128 new ArrayList<ResourceQualifier[]>(); 129 130 /** 131 * clipping value. If true, the rendering is limited to the screensize. This is the default 132 * value 133 */ 134 private boolean mClipping = true; 135 136 private final ConfigState mState = new ConfigState(); 137 138 private boolean mSdkChanged = false; 139 private boolean mFirstXmlModelChange = true; 140 141 /** The config listener given to the constructor. Never null. */ 142 private final IConfigListener mListener; 143 144 /** The {@link FolderConfiguration} representing the state of the UI controls */ 145 private final FolderConfiguration mCurrentConfig = new FolderConfiguration(); 146 147 /** The file being edited */ 148 private IFile mEditedFile; 149 /** The {@link ProjectResources} for the edited file's project */ 150 private ProjectResources mResources; 151 /** The target of the project of the file being edited. */ 152 private IAndroidTarget mTarget; 153 /** The {@link FolderConfiguration} being edited. */ 154 private FolderConfiguration mEditedConfig; 155 156 157 /** 158 * Interface implemented by the part which owns a {@link ConfigurationComposite}. 159 * This notifies the owners when the configuration change. 160 * The owner must also provide methods to provide the configuration that will 161 * be displayed. 162 */ 163 public interface IConfigListener { 164 void onConfigurationChange(); 165 void onThemeChange(); 166 void onCreate(); 167 void onClippingChange(); 168 169 ProjectResources getProjectResources(); 170 ProjectResources getFrameworkResources(); 171 Map<String, Map<String, IResourceValue>> getConfiguredProjectResources(); 172 Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources(); 173 } 174 175 /** 176 * State of the current config. This is used during UI reset to attempt to return the 177 * rendering to its original configuration. 178 */ 179 private class ConfigState { 180 private final static String SEP = ":"; //$NON-NLS-1$ 181 private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ 182 183 LayoutDevice device; 184 String configName; 185 ResourceQualifier[] locale; 186 String theme; 187 /** dock mode. Guaranteed to be non null */ 188 DockMode dock = DockMode.NONE; 189 /** night mode. Guaranteed to be non null */ 190 NightMode night = NightMode.NOTNIGHT; 191 192 String getData() { 193 StringBuilder sb = new StringBuilder(); 194 if (device != null) { 195 sb.append(device.getName()); 196 sb.append(SEP); 197 sb.append(configName); 198 sb.append(SEP); 199 if (locale != null) { 200 if (locale[0] != null && locale[1] != null) { 201 // locale[0]/[1] can be null sometimes when starting Eclipse 202 sb.append(((LanguageQualifier) locale[0]).getValue()); 203 sb.append(SEP_LOCALE); 204 sb.append(((RegionQualifier) locale[1]).getValue()); 205 } 206 } 207 sb.append(SEP); 208 sb.append(theme); 209 sb.append(SEP); 210 sb.append(dock.getResourceValue()); 211 sb.append(SEP); 212 sb.append(night.getResourceValue()); 213 sb.append(SEP); 214 } 215 216 return sb.toString(); 217 } 218 219 boolean setData(String data) { 220 String[] values = data.split(SEP); 221 if (values.length == 6) { 222 for (LayoutDevice d : mDeviceList) { 223 if (d.getName().equals(values[0])) { 224 device = d; 225 FolderConfiguration config = device.getConfigs().get(values[1]); 226 if (config != null) { 227 configName = values[1]; 228 229 locale = new ResourceQualifier[2]; 230 String locales[] = values[2].split(SEP_LOCALE); 231 if (locales.length >= 2) { 232 if (locales[0].length() > 0) { 233 locale[0] = new LanguageQualifier(locales[0]); 234 } 235 if (locales[1].length() > 0) { 236 locale[1] = new RegionQualifier(locales[1]); 237 } 238 } 239 240 theme = values[3]; 241 dock = DockMode.getEnum(values[4]); 242 if (dock == null) { 243 dock = DockMode.NONE; 244 } 245 night = NightMode.getEnum(values[5]); 246 if (night == null) { 247 night = NightMode.NOTNIGHT; 248 } 249 250 return true; 251 } 252 } 253 } 254 } 255 256 return false; 257 } 258 259 @Override 260 public String toString() { 261 StringBuilder sb = new StringBuilder(); 262 if (device != null) { 263 sb.append(device.getName()); 264 } else { 265 sb.append("null"); 266 } 267 sb.append(SEP); 268 sb.append(configName); 269 sb.append(SEP); 270 if (locale != null) { 271 sb.append(((LanguageQualifier) locale[0]).getValue()); 272 sb.append(SEP_LOCALE); 273 sb.append(((RegionQualifier) locale[1]).getValue()); 274 } 275 sb.append(SEP); 276 sb.append(theme); 277 sb.append(SEP); 278 sb.append(dock.getResourceValue()); 279 sb.append(SEP); 280 sb.append(night.getResourceValue()); 281 sb.append(SEP); 282 283 return sb.toString(); 284 } 285 } 286 287 /** 288 * Interface implemented by the part which owns a {@link ConfigurationComposite} 289 * to define and handle custom toggle buttons in the button bar. Each toggle is 290 * implemented using a button, with a callback when the button is selected. 291 */ 292 public static abstract class CustomToggle { 293 294 /** The UI label of the toggle. Can be null if the image exists. */ 295 private final String mUiLabel; 296 297 /** The image to use for this toggle. Can be null if the label exists. */ 298 private final Image mImage; 299 300 /** The tooltip for the toggle. Can be null. */ 301 private final String mUiTooltip; 302 303 /** 304 * Initializes a new {@link CustomToggle}. The values set here will be used 305 * later to create the actual toggle. 306 * 307 * @param uiLabel The UI label of the toggle. Can be null if the image exists. 308 * @param image The image to use for this toggle. Can be null if the label exists. 309 * @param uiTooltip The tooltip for the toggle. Can be null. 310 */ 311 public CustomToggle( 312 String uiLabel, 313 Image image, 314 String uiTooltip) { 315 mUiLabel = uiLabel; 316 mImage = image; 317 mUiTooltip = uiTooltip; 318 } 319 320 /** Called by the {@link ConfigurationComposite} when the button is selected. */ 321 public abstract void onSelected(boolean newState); 322 323 private void createToggle(Composite parent) { 324 final Button b = new Button(parent, SWT.TOGGLE | SWT.FLAT); 325 326 if (mUiTooltip != null) { 327 b.setToolTipText(mUiTooltip); 328 } 329 if (mImage != null) { 330 b.setImage(mImage); 331 } 332 if (mUiLabel != null) { 333 b.setText(mUiLabel); 334 } 335 336 b.addSelectionListener(new SelectionAdapter() { 337 @Override 338 public void widgetSelected(SelectionEvent e) { 339 onSelected(b.getSelection()); 340 } 341 }); 342 } 343 } 344 345 /** 346 * Creates a new {@link ConfigurationComposite} and adds it to the parent. 347 * 348 * @param listener An {@link IConfigListener} that gets and sets configuration properties. 349 * Mandatory, cannot be null. 350 * @param customToggles An array of {@link CustomToggle} to define extra toggles button 351 * to display at the top of the composite. Can be empty or null. 352 * @param parent The parent composite. 353 * @param style The style of this composite. 354 */ 355 public ConfigurationComposite(IConfigListener listener, 356 CustomToggle[] customToggles, 357 Composite parent, int style) { 358 super(parent, style); 359 mListener = listener; 360 361 if (customToggles == null) { 362 customToggles = new CustomToggle[0]; 363 } 364 365 GridLayout gl; 366 GridData gd; 367 int cols = 9; // device+config+locale+dock+day/night+separator*2+theme+createBtn 368 369 // ---- First line: custom buttons, clipping button, editing config display. 370 Composite labelParent = new Composite(this, SWT.NONE); 371 labelParent.setLayout(gl = new GridLayout(3 + customToggles.length, false)); 372 gl.marginWidth = gl.marginHeight = 0; 373 labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 374 gd.horizontalSpan = cols; 375 376 new Label(labelParent, SWT.NONE).setText("Editing config:"); 377 mCurrentLayoutLabel = new Label(labelParent, SWT.NONE); 378 mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 379 gd.widthHint = 50; 380 381 for (CustomToggle toggle : customToggles) { 382 toggle.createToggle(labelParent); 383 } 384 385 mClippingButton = new Button(labelParent, SWT.TOGGLE | SWT.FLAT); 386 mClippingButton.setSelection(mClipping); 387 mClippingButton.setToolTipText("Toggles screen clipping on/off"); 388 mClippingButton.setImage(IconFactory.getInstance().getIcon("clipping")); //$NON-NLS-1$ 389 mClippingButton.addSelectionListener(new SelectionAdapter() { 390 @Override 391 public void widgetSelected(SelectionEvent e) { 392 onClippingChange(); 393 } 394 }); 395 396 // ---- 2nd line: device/config/locale/theme Combos, create button. 397 398 setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 399 setLayout(gl = new GridLayout(cols, false)); 400 gl.marginHeight = 0; 401 gl.horizontalSpacing = 0; 402 403 mDeviceCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 404 mDeviceCombo.setLayoutData(new GridData( 405 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 406 mDeviceCombo.addSelectionListener(new SelectionAdapter() { 407 @Override 408 public void widgetSelected(SelectionEvent e) { 409 onDeviceChange(true /* recomputeLayout*/); 410 } 411 }); 412 413 mDeviceConfigCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 414 mDeviceConfigCombo.setLayoutData(new GridData( 415 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 416 mDeviceConfigCombo.addSelectionListener(new SelectionAdapter() { 417 @Override 418 public void widgetSelected(SelectionEvent e) { 419 onDeviceConfigChange(); 420 } 421 }); 422 423 mLocaleCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 424 mLocaleCombo.setLayoutData(new GridData( 425 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 426 mLocaleCombo.addSelectionListener(new SelectionListener() { 427 public void widgetDefaultSelected(SelectionEvent e) { 428 onLocaleChange(); 429 } 430 public void widgetSelected(SelectionEvent e) { 431 onLocaleChange(); 432 } 433 }); 434 435 mDockCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 436 mDockCombo.setLayoutData(new GridData( 437 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 438 for (DockMode mode : DockMode.values()) { 439 mDockCombo.add(mode.getLongDisplayValue()); 440 } 441 mDockCombo.addSelectionListener(new SelectionListener() { 442 public void widgetDefaultSelected(SelectionEvent e) { 443 onDockChange(); 444 } 445 public void widgetSelected(SelectionEvent e) { 446 onDockChange(); 447 } 448 }); 449 450 mNightCombo = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); 451 mNightCombo.setLayoutData(new GridData( 452 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 453 for (NightMode mode : NightMode.values()) { 454 mNightCombo.add(mode.getLongDisplayValue()); 455 } 456 mNightCombo.addSelectionListener(new SelectionListener() { 457 public void widgetDefaultSelected(SelectionEvent e) { 458 onDayChange(); 459 } 460 public void widgetSelected(SelectionEvent e) { 461 onDayChange(); 462 } 463 }); 464 465 // first separator 466 Label separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL); 467 separator.setLayoutData(gd = new GridData( 468 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); 469 gd.heightHint = 0; 470 471 mThemeCombo = new Combo(this, SWT.READ_ONLY | SWT.DROP_DOWN); 472 mThemeCombo.setLayoutData(new GridData( 473 GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); 474 mThemeCombo.setEnabled(false); 475 476 mThemeCombo.addSelectionListener(new SelectionAdapter() { 477 @Override 478 public void widgetSelected(SelectionEvent e) { 479 onThemeChange(); 480 } 481 }); 482 483 // second separator 484 separator = new Label(this, SWT.SEPARATOR | SWT.VERTICAL); 485 separator.setLayoutData(gd = new GridData( 486 GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); 487 gd.heightHint = 0; 488 489 mCreateButton = new Button(this, SWT.PUSH | SWT.FLAT); 490 mCreateButton.setText("Create..."); 491 mCreateButton.setEnabled(false); 492 mCreateButton.addSelectionListener(new SelectionAdapter() { 493 @Override 494 public void widgetSelected(SelectionEvent e) { 495 if (mListener != null) { 496 mListener.onCreate(); 497 } 498 } 499 }); 500 } 501 502 // ---- Init and reset/reload methods ---- 503 504 /** 505 * Sets the reference to the file being edited. 506 * <p/>The UI is intialized in {@link #onXmlModelLoaded()} which is called as the XML model is 507 * loaded (or reloaded as the SDK/target changes). 508 * 509 * @param file the file being opened 510 * 511 * @see #onXmlModelLoaded() 512 * @see #replaceFile(FolderConfiguration) 513 * @see #changeFileOnNewConfig(FolderConfiguration) 514 */ 515 public void setFile(IFile file) { 516 mEditedFile = file; 517 } 518 519 /** 520 * Replaces the UI with a given file configuration. This is meant to answer the user 521 * explicitly opening a different version of the same layout from the Package Explorer. 522 * <p/>This attempts to keep the current config, but may change it if it's not compatible or 523 * not the best match 524 * <p/>This will NOT trigger a redraw event (will not call 525 * {@link IConfigListener#onConfigurationChange()}.) 526 * @param file the file being opened. 527 * @param fileConfig The {@link FolderConfiguration} of the opened file. 528 * @param target the {@link IAndroidTarget} of the file's project. 529 * 530 * @see #replaceFile(FolderConfiguration) 531 */ 532 public void replaceFile(IFile file) { 533 // if there is no previous selection, revert to default mode. 534 if (mState.device == null) { 535 setFile(file); // onTargetChanged will be called later. 536 return; 537 } 538 539 mEditedFile = file; 540 IProject iProject = mEditedFile.getProject(); 541 mResources = ResourceManager.getInstance().getProjectResources(iProject); 542 543 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); 544 mEditedConfig = resFolder.getConfiguration(); 545 546 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 547 // new values in the widgets. 548 549 // only attempt to do anything if the SDK and targets are loaded. 550 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 551 if (sdkStatus == LoadStatus.LOADED) { 552 LoadStatus targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null); 553 554 if (targetStatus == LoadStatus.LOADED) { 555 556 // update the current config selection to make sure it's 557 // compatible with the new file 558 adaptConfigSelection(true /*needBestMatch*/); 559 560 // compute the final current config 561 computeCurrentConfig(true /*force*/); 562 563 // update the string showing the config value 564 updateConfigDisplay(mEditedConfig); 565 } 566 } 567 568 mDisableUpdates--; 569 } 570 571 /** 572 * Updates the UI with a new file that was opened in response to a config change. 573 * @param file the file being opened. 574 * 575 * @see #openFile(FolderConfiguration, IAndroidTarget) 576 * @see #replaceFile(FolderConfiguration) 577 */ 578 public void changeFileOnNewConfig(IFile file) { 579 mEditedFile = file; 580 IProject iProject = mEditedFile.getProject(); 581 mResources = ResourceManager.getInstance().getProjectResources(iProject); 582 583 ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); 584 mEditedConfig = resFolder.getConfiguration(); 585 586 // All that's needed is to update the string showing the config value 587 // (since the config combo were chosen by the user). 588 updateConfigDisplay(mEditedConfig); 589 } 590 591 /** 592 * Responds to the event that the basic SDK information finished loading. 593 * @param target the possibly new target object associated with the file being edited (in case 594 * the SDK path was changed). 595 */ 596 public void onSdkLoaded(IAndroidTarget target) { 597 // a change to the SDK means that we need to check for new/removed devices. 598 mSdkChanged = true; 599 600 // store the new target. 601 mTarget = target; 602 603 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 604 // new values in the widgets. 605 606 // this is going to be followed by a call to onTargetLoaded. 607 // So we can only care about the layout devices in this case. 608 initDevices(); 609 610 mDisableUpdates--; 611 } 612 613 /** 614 * Answers to the XML model being loaded, either the first time or when the Targget/SDK changes. 615 * <p>This initializes the UI, either with the first compatible configuration found, 616 * or attempts to restore a configuration if one is found to have been saved in the file 617 * persistent storage. 618 * <p>If the SDK or target are not loaded, nothing will happend (but the method must be called 619 * back when those are loaded). 620 * <p>The method automatically handles being called the first time after editor creation, or 621 * being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)} 622 * is properly called). 623 * 624 * @see #storeState() 625 * @see #onSdkLoaded(IAndroidTarget) 626 */ 627 public void onXmlModelLoaded() { 628 // only attempt to do anything if the SDK and targets are loaded. 629 LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); 630 if (sdkStatus == LoadStatus.LOADED) { 631 mDisableUpdates++; // we do not want to trigger onXXXChange when setting 632 633 // init the devices if needed (new SDK or first time going through here) 634 if (mSdkChanged || mFirstXmlModelChange) { 635 initDevices(); 636 } 637 638 IProject iProject = mEditedFile.getProject(); 639 640 Sdk currentSdk = Sdk.getCurrent(); 641 if (currentSdk != null) { 642 mTarget = currentSdk.getTarget(iProject); 643 } 644 645 LoadStatus targetStatus = LoadStatus.FAILED; 646 if (mTarget != null) { 647 targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mTarget, null); 648 } 649 650 if (targetStatus == LoadStatus.LOADED) { 651 if (mResources == null) { 652 mResources = ResourceManager.getInstance().getProjectResources(iProject); 653 } 654 if (mEditedConfig == null) { 655 ResourceFolder resFolder = mResources.getResourceFolder( 656 (IFolder) mEditedFile.getParent()); 657 mEditedConfig = resFolder.getConfiguration(); 658 } 659 660 // update the clipping state 661 AndroidTargetData targetData = Sdk.getCurrent().getTargetData(mTarget); 662 if (targetData != null) { 663 LayoutBridge bridge = targetData.getLayoutBridge(); 664 setClippingSupport(bridge.apiLevel >= 4); 665 } 666 667 // get the file stored state 668 boolean loadedConfigData = false; 669 try { 670 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE); 671 String data = mEditedFile.getPersistentProperty(qname); 672 if (data != null) { 673 loadedConfigData = mState.setData(data); 674 } 675 } catch (CoreException e) { 676 // pass 677 } 678 679 // update the themes and locales. 680 updateThemes(); 681 updateLocales(); 682 683 // If the current state was loaded from the persistent storage, we update the 684 // UI with it and then try to adapt it (which will handle incompatible 685 // configuration). 686 // Otherwise, just look for the first compatible configuration. 687 if (loadedConfigData) { 688 // first make sure we have the config to adapt 689 selectDevice(mState.device); 690 fillConfigCombo(mState.configName); 691 692 adaptConfigSelection(false /*needBestMatch*/); 693 694 mDockCombo.select(DockMode.getIndex(mState.dock)); 695 mNightCombo.select(NightMode.getIndex(mState.night)); 696 } else { 697 findAndSetCompatibleConfig(false /*favorCurrentConfig*/); 698 699 mDockCombo.select(0); 700 mNightCombo.select(0); 701 } 702 703 // update the string showing the config value 704 updateConfigDisplay(mEditedConfig); 705 706 // compute the final current config 707 computeCurrentConfig(true /*force*/); 708 } 709 710 mDisableUpdates--; 711 mFirstXmlModelChange = false; 712 } 713 } 714 715 /** 716 * Finds a device/config that can display {@link #mEditedConfig}. 717 * <p/>Once found the device and config combos are set to the config. 718 * <p/>If there is no compatible configuration, a custom one is created. 719 * @param favorCurrentConfig if true, and no best match is found, don't change 720 * the current config. This must only be true if the current config is compatible. 721 */ 722 private void findAndSetCompatibleConfig(boolean favorCurrentConfig) { 723 LayoutDevice anyDeviceMatch = null; // a compatible device/config/locale 724 String anyConfigMatchName = null; 725 int anyLocaleIndex = -1; 726 727 LayoutDevice bestDeviceMatch = null; // an actual best match 728 String bestConfigMatchName = null; 729 int bestLocaleIndex = -1; 730 731 FolderConfiguration testConfig = new FolderConfiguration(); 732 733 mainloop: for (LayoutDevice device : mDeviceList) { 734 for (Entry<String, FolderConfiguration> entry : 735 device.getConfigs().entrySet()) { 736 testConfig.set(entry.getValue()); 737 738 // look on the locales. 739 for (int i = 0 ; i < mLocaleList.size() ; i++) { 740 ResourceQualifier[] locale = mLocaleList.get(i); 741 742 // update the test config with the locale qualifiers 743 testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]); 744 testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]); 745 746 if (mEditedConfig.isMatchFor(testConfig)) { 747 // this is a basic match. record it in case we don't find a match 748 // where the edited file is a best config. 749 if (anyDeviceMatch == null) { 750 anyDeviceMatch = device; 751 anyConfigMatchName = entry.getKey(); 752 anyLocaleIndex = i; 753 } 754 755 if (isCurrentFileBestMatchFor(testConfig)) { 756 // this is what we want. 757 bestDeviceMatch = device; 758 bestConfigMatchName = entry.getKey(); 759 bestLocaleIndex = i; 760 break mainloop; 761 } 762 } 763 } 764 } 765 } 766 767 if (bestDeviceMatch == null) { 768 if (favorCurrentConfig) { 769 // quick check 770 if (mEditedConfig.isMatchFor(mCurrentConfig) == false) { 771 AdtPlugin.log(IStatus.ERROR, 772 "favorCurrentConfig can only be true if the current config is compatible"); 773 } 774 775 // just display the warning 776 AdtPlugin.printErrorToConsole(mEditedFile.getProject(), 777 String.format( 778 "'%1$s' is not a best match for any device/locale combination.", 779 mEditedConfig.toDisplayString()), 780 String.format( 781 "Displaying it with '%1$s'", 782 mCurrentConfig.toDisplayString())); 783 } else if (anyDeviceMatch != null) { 784 // select the device anyway. 785 selectDevice(mState.device = anyDeviceMatch); 786 fillConfigCombo(anyConfigMatchName); 787 mLocaleCombo.select(anyLocaleIndex); 788 789 // TODO: display a better warning! 790 computeCurrentConfig(false /*force*/); 791 AdtPlugin.printErrorToConsole(mEditedFile.getProject(), 792 String.format( 793 "'%1$s' is not a best match for any device/locale combination.", 794 mEditedConfig.toDisplayString()), 795 String.format( 796 "Displaying it with '%1$s'", 797 mCurrentConfig.toDisplayString())); 798 799 } else { 800 // TODO: there is no device/config able to display the layout, create one. 801 // For the base config values, we'll take the first device and config, 802 // and replace whatever qualifier required by the layout file. 803 } 804 } else { 805 selectDevice(mState.device = bestDeviceMatch); 806 fillConfigCombo(bestConfigMatchName); 807 mLocaleCombo.select(bestLocaleIndex); 808 } 809 } 810 811 /** 812 * Adapts the current device/config selection so that it's compatible with 813 * {@link #mEditedConfig}. 814 * <p/>If the current selection is compatible, nothing is changed. 815 * <p/>If it's not compatible, configs from the current devices are tested. 816 * <p/>If none are compatible, it reverts to 817 * {@link #findAndSetCompatibleConfig(FolderConfiguration)} 818 */ 819 private void adaptConfigSelection(boolean needBestMatch) { 820 // check the device config (ie sans locale) 821 boolean needConfigChange = true; // if still true, we need to find another config. 822 boolean currentConfigIsCompatible = false; 823 int configIndex = mDeviceConfigCombo.getSelectionIndex(); 824 if (configIndex != -1) { 825 String configName = mDeviceConfigCombo.getItem(configIndex); 826 FolderConfiguration currentConfig = mState.device.getConfigs().get(configName); 827 if (mEditedConfig.isMatchFor(currentConfig)) { 828 currentConfigIsCompatible = true; // current config is compatible 829 if (needBestMatch == false || isCurrentFileBestMatchFor(currentConfig)) { 830 needConfigChange = false; 831 } 832 } 833 } 834 835 if (needConfigChange) { 836 // if the current config/locale isn't a correct match, then 837 // look for another config/locale in the same device. 838 FolderConfiguration testConfig = new FolderConfiguration(); 839 840 // first look in the current device. 841 String matchName = null; 842 int localeIndex = -1; 843 Map<String, FolderConfiguration> configs = mState.device.getConfigs(); 844 mainloop: for (Entry<String, FolderConfiguration> entry : configs.entrySet()) { 845 testConfig.set(entry.getValue()); 846 847 // loop on the locales. 848 for (int i = 0 ; i < mLocaleList.size() ; i++) { 849 ResourceQualifier[] locale = mLocaleList.get(i); 850 851 // update the test config with the locale qualifiers 852 testConfig.setLanguageQualifier((LanguageQualifier)locale[LOCALE_LANG]); 853 testConfig.setRegionQualifier((RegionQualifier)locale[LOCALE_REGION]); 854 855 if (mEditedConfig.isMatchFor(testConfig) && 856 isCurrentFileBestMatchFor(testConfig)) { 857 matchName = entry.getKey(); 858 localeIndex = i; 859 break mainloop; 860 } 861 } 862 } 863 864 if (matchName != null) { 865 selectConfig(matchName); 866 mLocaleCombo.select(localeIndex); 867 } else { 868 // no match in current device with any config/locale 869 // attempt to find another device that can display this particular config. 870 findAndSetCompatibleConfig(currentConfigIsCompatible); 871 } 872 } 873 } 874 875 /** 876 * Finds a locale matching the config from a file. 877 * @param language the language qualifier or null if none is set. 878 * @param region the region qualifier or null if none is set. 879 */ 880 private void setLocaleCombo(ResourceQualifier language, ResourceQualifier region) { 881 // find the locale match. Since the locale list is based on the content of the 882 // project resources there must be an exact match. 883 // The only trick is that the region could be null in the fileConfig but in our 884 // list of locales, this is represented as a RegionQualifier with value of 885 // FAKE_LOCALE_VALUE. 886 final int count = mLocaleList.size(); 887 for (int i = 0 ; i < count ; i++) { 888 ResourceQualifier[] locale = mLocaleList.get(i); 889 890 // the language qualifier in the locale list is never null. 891 if (locale[LOCALE_LANG].equals(language)) { 892 // region comparison is more complex, as the region could be null. 893 if (region == null) { 894 if (RegionQualifier.FAKE_REGION_VALUE.equals( 895 ((RegionQualifier)locale[LOCALE_REGION]).getValue())) { 896 // match! 897 mLocaleCombo.select(i); 898 break; 899 } 900 } else if (region.equals(locale[LOCALE_REGION])) { 901 // match! 902 mLocaleCombo.select(i); 903 break; 904 } 905 } 906 } 907 } 908 909 private void updateConfigDisplay(FolderConfiguration fileConfig) { 910 String current = fileConfig.toDisplayString(); 911 mCurrentLayoutLabel.setText(current != null ? current : "(Default)"); 912 } 913 914 private void saveState(boolean force) { 915 if (mDisableUpdates == 0) { 916 int index = mDeviceConfigCombo.getSelectionIndex(); 917 if (index != -1) { 918 mState.configName = mDeviceConfigCombo.getItem(index); 919 } else { 920 mState.configName = null; 921 } 922 923 // since the locales are relative to the project, only keeping the index is enough 924 index = mLocaleCombo.getSelectionIndex(); 925 if (index != -1) { 926 mState.locale = mLocaleList.get(index); 927 } else { 928 mState.locale = null; 929 } 930 931 index = mThemeCombo.getSelectionIndex(); 932 if (index != -1) { 933 mState.theme = mThemeCombo.getItem(index); 934 } 935 936 index = mDockCombo.getSelectionIndex(); 937 if (index != -1) { 938 mState.dock = DockMode.getByIndex(index); 939 } 940 941 index = mNightCombo.getSelectionIndex(); 942 if (index != -1) { 943 mState.night = NightMode.getByIndex(index); 944 } 945 } 946 } 947 948 /** 949 * Stores the current config selection into the edited file. 950 */ 951 public void storeState() { 952 try { 953 QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE); //$NON-NLS-1$ 954 mEditedFile.setPersistentProperty(qname, mState.getData()); 955 } catch (CoreException e) { 956 // pass 957 } 958 } 959 960 /** 961 * Updates the locale combo. 962 * This must be called from the UI thread. 963 */ 964 public void updateLocales() { 965 if (mListener == null) { 966 return; // can't do anything w/o it. 967 } 968 969 mDisableUpdates++; 970 971 // Reset the combo 972 mLocaleCombo.removeAll(); 973 mLocaleList.clear(); 974 975 SortedSet<String> languages = null; 976 boolean hasLocale = false; 977 978 // get the languages from the project. 979 ProjectResources project = mListener.getProjectResources(); 980 981 // in cases where the opened file is not linked to a project, this could be null. 982 if (project != null) { 983 // now get the languages from the project. 984 languages = project.getLanguages(); 985 986 for (String language : languages) { 987 hasLocale = true; 988 989 LanguageQualifier langQual = new LanguageQualifier(language); 990 991 // find the matching regions and add them 992 SortedSet<String> regions = project.getRegions(language); 993 for (String region : regions) { 994 mLocaleCombo.add(String.format("%1$s / %2$s", language, region)); //$NON-NLS-1$ 995 RegionQualifier regionQual = new RegionQualifier(region); 996 mLocaleList.add(new ResourceQualifier[] { langQual, regionQual }); 997 } 998 999 // now the entry for the other regions the language alone 1000 if (regions.size() > 0) { 1001 mLocaleCombo.add(String.format("%1$s / Other", language)); //$NON-NLS-1$ 1002 } else { 1003 mLocaleCombo.add(String.format("%1$s / Any", language)); //$NON-NLS-1$ 1004 } 1005 // create a region qualifier that will never be matched by qualified resources. 1006 mLocaleList.add(new ResourceQualifier[] { 1007 langQual, 1008 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) 1009 }); 1010 } 1011 } 1012 1013 // add a locale not present in the project resources. This will let the dev 1014 // tests his/her default values. 1015 if (hasLocale) { 1016 mLocaleCombo.add("Other"); 1017 } else { 1018 mLocaleCombo.add("Any locale"); 1019 } 1020 1021 // create language/region qualifier that will never be matched by qualified resources. 1022 mLocaleList.add(new ResourceQualifier[] { 1023 new LanguageQualifier(LanguageQualifier.FAKE_LANG_VALUE), 1024 new RegionQualifier(RegionQualifier.FAKE_REGION_VALUE) 1025 }); 1026 1027 if (mState.locale != null) { 1028 // FIXME: this may fails if the layout was deleted (and was the last one to have that local. 1029 // (we have other problem in this case though) 1030 setLocaleCombo(mState.locale[LOCALE_LANG], 1031 mState.locale[LOCALE_REGION]); 1032 } else { 1033 mLocaleCombo.select(0); 1034 } 1035 1036 mThemeCombo.getParent().layout(); 1037 1038 mDisableUpdates--; 1039 } 1040 1041 /** 1042 * Updates the theme combo. 1043 * This must be called from the UI thread. 1044 */ 1045 private void updateThemes() { 1046 if (mListener == null) { 1047 return; // can't do anything w/o it. 1048 } 1049 1050 ProjectResources frameworkProject = mListener.getFrameworkResources(); 1051 1052 mDisableUpdates++; 1053 1054 // Reset the combo 1055 mThemeCombo.removeAll(); 1056 mPlatformThemeCount = 0; 1057 1058 ArrayList<String> themes = new ArrayList<String>(); 1059 1060 // get the themes, and languages from the Framework. 1061 if (frameworkProject != null) { 1062 // get the configured resources for the framework 1063 Map<String, Map<String, IResourceValue>> frameworResources = 1064 mListener.getConfiguredFrameworkResources(); 1065 1066 if (frameworResources != null) { 1067 // get the styles. 1068 Map<String, IResourceValue> styles = frameworResources.get( 1069 ResourceType.STYLE.getName()); 1070 1071 1072 // collect the themes out of all the styles. 1073 for (IResourceValue value : styles.values()) { 1074 String name = value.getName(); 1075 if (name.startsWith("Theme.") || name.equals("Theme")) { 1076 themes.add(value.getName()); 1077 mPlatformThemeCount++; 1078 } 1079 } 1080 1081 // sort them and add them to the combo 1082 Collections.sort(themes); 1083 1084 for (String theme : themes) { 1085 mThemeCombo.add(theme); 1086 } 1087 1088 mPlatformThemeCount = themes.size(); 1089 themes.clear(); 1090 } 1091 } 1092 1093 // now get the themes and languages from the project. 1094 ProjectResources project = mListener.getProjectResources(); 1095 // in cases where the opened file is not linked to a project, this could be null. 1096 if (project != null) { 1097 // get the configured resources for the project 1098 Map<String, Map<String, IResourceValue>> configuredProjectRes = 1099 mListener.getConfiguredProjectResources(); 1100 1101 if (configuredProjectRes != null) { 1102 // get the styles. 1103 Map<String, IResourceValue> styleMap = configuredProjectRes.get( 1104 ResourceType.STYLE.getName()); 1105 1106 if (styleMap != null) { 1107 // collect the themes out of all the styles, ie styles that extend, 1108 // directly or indirectly a platform theme. 1109 for (IResourceValue value : styleMap.values()) { 1110 if (isTheme(value, styleMap)) { 1111 themes.add(value.getName()); 1112 } 1113 } 1114 1115 // sort them and add them the to the combo. 1116 if (mPlatformThemeCount > 0 && themes.size() > 0) { 1117 mThemeCombo.add(THEME_SEPARATOR); 1118 } 1119 1120 Collections.sort(themes); 1121 1122 for (String theme : themes) { 1123 mThemeCombo.add(theme); 1124 } 1125 } 1126 } 1127 } 1128 1129 // try to reselect the previous theme. 1130 if (mState.theme != null) { 1131 final int count = mThemeCombo.getItemCount(); 1132 for (int i = 0 ; i < count ; i++) { 1133 if (mState.theme.equals(mThemeCombo.getItem(i))) { 1134 mThemeCombo.select(i); 1135 break; 1136 } 1137 } 1138 mThemeCombo.setEnabled(true); 1139 } else if (mThemeCombo.getItemCount() > 0) { 1140 mThemeCombo.select(0); 1141 mThemeCombo.setEnabled(true); 1142 } else { 1143 mThemeCombo.setEnabled(false); 1144 } 1145 1146 mThemeCombo.getParent().layout(); 1147 1148 mDisableUpdates--; 1149 } 1150 1151 // ---- getters for the config selection values ---- 1152 1153 public FolderConfiguration getEditedConfig() { 1154 return mEditedConfig; 1155 } 1156 1157 public FolderConfiguration getCurrentConfig() { 1158 return mCurrentConfig; 1159 } 1160 1161 public void getCurrentConfig(FolderConfiguration config) { 1162 config.set(mCurrentConfig); 1163 } 1164 1165 /** 1166 * Returns the currently selected {@link Density}. This is guaranteed to be non null. 1167 */ 1168 public Density getDensity() { 1169 if (mCurrentConfig != null) { 1170 PixelDensityQualifier qual = mCurrentConfig.getPixelDensityQualifier(); 1171 if (qual != null) { 1172 // just a sanity check 1173 Density d = qual.getValue(); 1174 if (d != Density.NODPI) { 1175 return d; 1176 } 1177 } 1178 } 1179 1180 // no config? return medium as the default density. 1181 return Density.MEDIUM; 1182 } 1183 1184 /** 1185 * Returns the current device xdpi. 1186 */ 1187 public float getXDpi() { 1188 if (mState.device != null) { 1189 float dpi = mState.device.getXDpi(); 1190 if (Float.isNaN(dpi) == false) { 1191 return dpi; 1192 } 1193 } 1194 1195 // get the pixel density as the density. 1196 return getDensity().getDpiValue(); 1197 } 1198 1199 /** 1200 * Returns the current device ydpi. 1201 */ 1202 public float getYDpi() { 1203 if (mState.device != null) { 1204 float dpi = mState.device.getYDpi(); 1205 if (Float.isNaN(dpi) == false) { 1206 return dpi; 1207 } 1208 } 1209 1210 // get the pixel density as the density. 1211 return getDensity().getDpiValue(); 1212 } 1213 1214 public Rectangle getScreenBounds() { 1215 // get the orientation from the current device config 1216 ScreenOrientationQualifier qual = mCurrentConfig.getScreenOrientationQualifier(); 1217 ScreenOrientation orientation = ScreenOrientation.PORTRAIT; 1218 if (qual != null) { 1219 orientation = qual.getValue(); 1220 } 1221 1222 // get the device screen dimension 1223 ScreenDimensionQualifier qual2 = mCurrentConfig.getScreenDimensionQualifier(); 1224 int s1, s2; 1225 if (qual2 != null) { 1226 s1 = qual2.getValue1(); 1227 s2 = qual2.getValue2(); 1228 } else { 1229 s1 = 480; 1230 s2 = 320; 1231 } 1232 1233 switch (orientation) { 1234 default: 1235 case PORTRAIT: 1236 return new Rectangle(0, 0, s2, s1); 1237 case LANDSCAPE: 1238 return new Rectangle(0, 0, s1, s2); 1239 case SQUARE: 1240 return new Rectangle(0, 0, s1, s1); 1241 } 1242 } 1243 1244 1245 /** 1246 * Returns the current theme, or null if the combo has no selection. 1247 */ 1248 public String getTheme() { 1249 int themeIndex = mThemeCombo.getSelectionIndex(); 1250 if (themeIndex != -1) { 1251 return mThemeCombo.getItem(themeIndex); 1252 } 1253 1254 return null; 1255 } 1256 1257 /** 1258 * Returns whether the current theme selection is a project theme. 1259 * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>. 1260 * @return true for project theme, false for framework theme 1261 */ 1262 public boolean isProjectTheme() { 1263 return mThemeCombo.getSelectionIndex() >= mPlatformThemeCount; 1264 } 1265 1266 public boolean getClipping() { 1267 return mClipping; 1268 } 1269 1270 private void setClippingSupport(boolean b) { 1271 mClippingButton.setEnabled(b); 1272 if (b) { 1273 mClippingButton.setToolTipText("Toggles screen clipping on/off"); 1274 } else { 1275 mClipping = true; 1276 mClippingButton.setSelection(true); 1277 mClippingButton.setToolTipText("Non clipped rendering is not supported"); 1278 } 1279 } 1280 1281 /** 1282 * Loads the list of {@link LayoutDevice} and inits the UI with it. 1283 */ 1284 private void initDevices() { 1285 mDeviceList = null; 1286 1287 Sdk sdk = Sdk.getCurrent(); 1288 if (sdk != null) { 1289 LayoutDeviceManager manager = sdk.getLayoutDeviceManager(); 1290 mDeviceList = manager.getCombinedList(); 1291 } 1292 1293 1294 // remove older devices if applicable 1295 mDeviceCombo.removeAll(); 1296 mDeviceConfigCombo.removeAll(); 1297 1298 // fill with the devices 1299 if (mDeviceList != null) { 1300 for (LayoutDevice device : mDeviceList) { 1301 mDeviceCombo.add(device.getName()); 1302 } 1303 mDeviceCombo.select(0); 1304 1305 if (mDeviceList.size() > 0) { 1306 Map<String, FolderConfiguration> configs = mDeviceList.get(0).getConfigs(); 1307 Set<String> configNames = configs.keySet(); 1308 for (String name : configNames) { 1309 mDeviceConfigCombo.add(name); 1310 } 1311 mDeviceConfigCombo.select(0); 1312 if (configNames.size() == 1) { 1313 mDeviceConfigCombo.setEnabled(false); 1314 } 1315 } 1316 } 1317 1318 // add the custom item 1319 mDeviceCombo.add("Custom..."); 1320 } 1321 1322 /** 1323 * Selects a given {@link LayoutDevice} in the device combo, if it is found. 1324 * @param device the device to select 1325 * @return true if the device was found. 1326 */ 1327 private boolean selectDevice(LayoutDevice device) { 1328 final int count = mDeviceList.size(); 1329 for (int i = 0 ; i < count ; i++) { 1330 // since device comes from mDeviceList, we can use the == operator. 1331 if (device == mDeviceList.get(i)) { 1332 mDeviceCombo.select(i); 1333 return true; 1334 } 1335 } 1336 1337 return false; 1338 } 1339 1340 /** 1341 * Selects a config by name. 1342 * @param name the name of the config to select. 1343 */ 1344 private void selectConfig(String name) { 1345 final int count = mDeviceConfigCombo.getItemCount(); 1346 for (int i = 0 ; i < count ; i++) { 1347 String item = mDeviceConfigCombo.getItem(i); 1348 if (name.equals(item)) { 1349 mDeviceConfigCombo.select(i); 1350 return; 1351 } 1352 } 1353 } 1354 1355 /** 1356 * Called when the selection of the device combo changes. 1357 * @param recomputeLayout 1358 */ 1359 private void onDeviceChange(boolean recomputeLayout) { 1360 // because changing the content of a combo triggers a change event, respect the 1361 // mDisableUpdates flag 1362 if (mDisableUpdates > 0) { 1363 return; 1364 } 1365 1366 String newConfigName = null; 1367 1368 int deviceIndex = mDeviceCombo.getSelectionIndex(); 1369 if (deviceIndex != -1) { 1370 // check if the user is asking for the custom item 1371 if (deviceIndex == mDeviceCombo.getItemCount() - 1) { 1372 onCustomDeviceConfig(); 1373 return; 1374 } 1375 1376 // get the previous config, so that we can look for a close match 1377 if (mState.device != null) { 1378 int index = mDeviceConfigCombo.getSelectionIndex(); 1379 if (index != -1) { 1380 FolderConfiguration oldConfig = mState.device.getConfigs().get( 1381 mDeviceConfigCombo.getItem(index)); 1382 1383 LayoutDevice newDevice = mDeviceList.get(deviceIndex); 1384 1385 newConfigName = getClosestMatch(oldConfig, newDevice.getConfigs()); 1386 } 1387 } 1388 1389 mState.device = mDeviceList.get(deviceIndex); 1390 } else { 1391 mState.device = null; 1392 } 1393 1394 fillConfigCombo(newConfigName); 1395 1396 computeCurrentConfig(false /*force*/); 1397 1398 if (recomputeLayout) { 1399 onDeviceConfigChange(); 1400 } 1401 } 1402 1403 /** 1404 * Handles a user request for the {@link ConfigManagerDialog}. 1405 */ 1406 private void onCustomDeviceConfig() { 1407 ConfigManagerDialog dialog = new ConfigManagerDialog(getShell()); 1408 dialog.open(); 1409 1410 // save the user devices 1411 Sdk.getCurrent().getLayoutDeviceManager().save(); 1412 1413 // Update the UI with no triggered event 1414 mDisableUpdates++; 1415 1416 LayoutDevice oldCurrent = mState.device; 1417 1418 // but first, update the device combo 1419 initDevices(); 1420 1421 // attempts to reselect the current device. 1422 if (selectDevice(oldCurrent)) { 1423 // current device still exists. 1424 // reselect the config 1425 selectConfig(mState.configName); 1426 1427 // reset the UI as if it was just a replacement file, since we can keep 1428 // the current device (and possibly config). 1429 adaptConfigSelection(false /*needBestMatch*/); 1430 1431 } else { 1432 // find a new device/config to match the current file. 1433 findAndSetCompatibleConfig(false /*favorCurrentConfig*/); 1434 } 1435 1436 mDisableUpdates--; 1437 1438 // recompute the current config 1439 computeCurrentConfig(false /*force*/); 1440 1441 // force a redraw 1442 onDeviceChange(true /*recomputeLayout*/); 1443 } 1444 1445 /** 1446 * Attempts to find a close config among a list 1447 * @param oldConfig the reference config. 1448 * @param configs the list of config to search through 1449 * @return the name of the closest config match, or possibly null if no configs are compatible 1450 * (this can only happen if the configs don't have a single qualifier that is the same). 1451 */ 1452 private String getClosestMatch(FolderConfiguration oldConfig, 1453 Map<String, FolderConfiguration> configs) { 1454 1455 // create 2 lists as we're going to go through one and put the candidates in the other. 1456 ArrayList<Entry<String, FolderConfiguration>> list1 = 1457 new ArrayList<Entry<String,FolderConfiguration>>(); 1458 ArrayList<Entry<String, FolderConfiguration>> list2 = 1459 new ArrayList<Entry<String,FolderConfiguration>>(); 1460 1461 list1.addAll(configs.entrySet()); 1462 1463 final int count = FolderConfiguration.getQualifierCount(); 1464 for (int i = 0 ; i < count ; i++) { 1465 // compute the new candidate list by only taking configs that have 1466 // the same i-th qualifier as the old config 1467 for (Entry<String, FolderConfiguration> entry : list1) { 1468 ResourceQualifier oldQualifier = oldConfig.getQualifier(i); 1469 1470 FolderConfiguration config = entry.getValue(); 1471 ResourceQualifier newQualifier = config.getQualifier(i); 1472 1473 if (oldQualifier == null) { 1474 if (newQualifier == null) { 1475 list2.add(entry); 1476 } 1477 } else if (oldQualifier.equals(newQualifier)) { 1478 list2.add(entry); 1479 } 1480 } 1481 1482 // at any moment if the new candidate list contains only one match, its name 1483 // is returned. 1484 if (list2.size() == 1) { 1485 return list2.get(0).getKey(); 1486 } 1487 1488 // if the list is empty, then all the new configs failed. It is considered ok, and 1489 // we move to the next qualifier anyway. This way, if a qualifier is different for 1490 // all new configs it is simply ignored. 1491 if (list2.size() != 0) { 1492 // move the candidates back into list1. 1493 list1.clear(); 1494 list1.addAll(list2); 1495 list2.clear(); 1496 } 1497 } 1498 1499 // the only way to reach this point is if there's an exact match. 1500 // (if there are more than one, then there's a duplicate config and it doesn't matter, 1501 // we take the first one). 1502 if (list1.size() > 0) { 1503 return list1.get(0).getKey(); 1504 } 1505 1506 return null; 1507 } 1508 1509 /** 1510 * fills the config combo with new values based on {@link #mCurrentState#device}. 1511 * @param refName an optional name. if set the selection will match this name (if found) 1512 */ 1513 private void fillConfigCombo(String refName) { 1514 mDeviceConfigCombo.removeAll(); 1515 1516 if (mState.device != null) { 1517 Set<String> configNames = mState.device.getConfigs().keySet(); 1518 1519 int selectionIndex = 0; 1520 int i = 0; 1521 1522 for (String name : configNames) { 1523 mDeviceConfigCombo.add(name); 1524 1525 if (name.equals(refName)) { 1526 selectionIndex = i; 1527 } 1528 i++; 1529 } 1530 1531 mDeviceConfigCombo.select(selectionIndex); 1532 mDeviceConfigCombo.setEnabled(configNames.size() > 1); 1533 } 1534 } 1535 1536 /** 1537 * Called when the device config selection changes. 1538 */ 1539 private void onDeviceConfigChange() { 1540 // because changing the content of a combo triggers a change event, respect the 1541 // mDisableUpdates flag 1542 if (mDisableUpdates > 0) { 1543 return; 1544 } 1545 1546 if (computeCurrentConfig(false /*force*/) && mListener != null) { 1547 mListener.onConfigurationChange(); 1548 } 1549 } 1550 1551 /** 1552 * Call back for language combo selection 1553 */ 1554 private void onLocaleChange() { 1555 // because mLocaleList triggers onLanguageChange at each modification, the filling 1556 // of the combo with data will trigger notifications, and we don't want that. 1557 if (mDisableUpdates > 0) { 1558 return; 1559 } 1560 1561 if (computeCurrentConfig(false /*force*/) && mListener != null) { 1562 mListener.onConfigurationChange(); 1563 } 1564 } 1565 1566 private void onDockChange() { 1567 if (computeCurrentConfig(false /*force*/) && mListener != null) { 1568 mListener.onConfigurationChange(); 1569 } 1570 } 1571 1572 private void onDayChange() { 1573 if (computeCurrentConfig(false /*force*/) && mListener != null) { 1574 mListener.onConfigurationChange(); 1575 } 1576 } 1577 1578 /** 1579 * Saves the current state and the current configuration 1580 * @param force forces saving the states even if updates are disabled 1581 * 1582 * @see #saveState(boolean) 1583 */ 1584 private boolean computeCurrentConfig(boolean force) { 1585 saveState(force); 1586 1587 if (mState.device != null) { 1588 // get the device config from the device/config combos. 1589 int configIndex = mDeviceConfigCombo.getSelectionIndex(); 1590 String name = mDeviceConfigCombo.getItem(configIndex); 1591 FolderConfiguration config = mState.device.getConfigs().get(name); 1592 1593 // replace the config with the one from the device 1594 mCurrentConfig.set(config); 1595 1596 // replace the locale qualifiers with the one coming from the locale combo 1597 int localeIndex = mLocaleCombo.getSelectionIndex(); 1598 if (localeIndex != -1) { 1599 ResourceQualifier[] localeQualifiers = mLocaleList.get(localeIndex); 1600 1601 mCurrentConfig.setLanguageQualifier( 1602 (LanguageQualifier)localeQualifiers[LOCALE_LANG]); 1603 mCurrentConfig.setRegionQualifier( 1604 (RegionQualifier)localeQualifiers[LOCALE_REGION]); 1605 } 1606 1607 int index = mDockCombo.getSelectionIndex(); 1608 if (index == -1) { 1609 index = 0; // no selection = 0 1610 } 1611 mCurrentConfig.setDockModeQualifier(new DockModeQualifier(DockMode.getByIndex(index))); 1612 1613 index = mNightCombo.getSelectionIndex(); 1614 if (index == -1) { 1615 index = 0; // no selection = 0 1616 } 1617 mCurrentConfig.setNightModeQualifier( 1618 new NightModeQualifier(NightMode.getByIndex(index))); 1619 1620 // update the create button. 1621 checkCreateEnable(); 1622 1623 return true; 1624 } 1625 1626 return false; 1627 } 1628 1629 private void onThemeChange() { 1630 saveState(false /*force*/); 1631 1632 int themeIndex = mThemeCombo.getSelectionIndex(); 1633 if (themeIndex != -1) { 1634 String theme = mThemeCombo.getItem(themeIndex); 1635 1636 if (theme.equals(THEME_SEPARATOR)) { 1637 mThemeCombo.select(0); 1638 } 1639 1640 if (mListener != null) { 1641 mListener.onThemeChange(); 1642 } 1643 } 1644 } 1645 1646 private void onClippingChange() { 1647 mClipping = mClippingButton.getSelection(); 1648 if (mListener != null) { 1649 mListener.onClippingChange(); 1650 } 1651 } 1652 1653 /** 1654 * Returns whether the given <var>style</var> is a theme. 1655 * This is done by making sure the parent is a theme. 1656 * @param value the style to check 1657 * @param styleMap the map of styles for the current project. Key is the style name. 1658 * @return True if the given <var>style</var> is a theme. 1659 */ 1660 private boolean isTheme(IResourceValue value, Map<String, IResourceValue> styleMap) { 1661 if (value instanceof IStyleResourceValue) { 1662 IStyleResourceValue style = (IStyleResourceValue)value; 1663 1664 boolean frameworkStyle = false; 1665 String parentStyle = style.getParentStyle(); 1666 if (parentStyle == null) { 1667 // if there is no specified parent style we look an implied one. 1668 // For instance 'Theme.light' is implied child style of 'Theme', 1669 // and 'Theme.light.fullscreen' is implied child style of 'Theme.light' 1670 String name = style.getName(); 1671 int index = name.lastIndexOf('.'); 1672 if (index != -1) { 1673 parentStyle = name.substring(0, index); 1674 } 1675 } else { 1676 // remove the useless @ if it's there 1677 if (parentStyle.startsWith("@")) { 1678 parentStyle = parentStyle.substring(1); 1679 } 1680 1681 // check for framework identifier. 1682 if (parentStyle.startsWith("android:")) { 1683 frameworkStyle = true; 1684 parentStyle = parentStyle.substring("android:".length()); 1685 } 1686 1687 // at this point we could have the format style/<name>. we want only the name 1688 if (parentStyle.startsWith("style/")) { 1689 parentStyle = parentStyle.substring("style/".length()); 1690 } 1691 } 1692 1693 if (parentStyle != null) { 1694 if (frameworkStyle) { 1695 // if the parent is a framework style, it has to be 'Theme' or 'Theme.*' 1696 return parentStyle.equals("Theme") || parentStyle.startsWith("Theme."); 1697 } else { 1698 // if it's a project style, we check this is a theme. 1699 value = styleMap.get(parentStyle); 1700 if (value != null) { 1701 return isTheme(value, styleMap); 1702 } 1703 } 1704 } 1705 } 1706 1707 return false; 1708 } 1709 1710 private void checkCreateEnable() { 1711 mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false); 1712 } 1713 1714 /** 1715 * Checks whether the current edited file is the best match for a given config. 1716 * <p/> 1717 * This tests against other versions of the same layout in the project. 1718 * <p/> 1719 * The given config must be compatible with the current edited file. 1720 * @param config the config to test. 1721 * @return true if the current edited file is the best match in the project for the 1722 * given config. 1723 */ 1724 private boolean isCurrentFileBestMatchFor(FolderConfiguration config) { 1725 ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), 1726 ResourceFolderType.LAYOUT, config); 1727 1728 if (match != null) { 1729 return match.getFile().equals(mEditedFile); 1730 } else { 1731 // if we stop here that means the current file is not even a match! 1732 AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config."); 1733 } 1734 1735 return false; 1736 } 1737 } 1738 1739