/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdkuilib.internal.widgets; import com.android.sdklib.internal.avd.AvdManager; import com.android.sdklib.internal.avd.AvdManager.AvdInfo; import com.android.sdkuilib.internal.repository.SettingsController; import com.android.sdkuilib.ui.GridDialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.awt.Toolkit; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Dialog dealing with emulator launch options. The following options are supported: * <ul> * <li>-wipe-data</li> * <li>-scale</li> * </ul> * Values are stored (in the class as static field) to be reused while the app is still running. * The Monitor dpi is stored in the settings if available. */ final class AvdStartDialog extends GridDialog { // static field to reuse values during the same session. private static boolean sWipeData = false; private static boolean sSnapshotSave = true; private static boolean sSnapshotLaunch = true; private static int sMonitorDpi = 72; // used if there's no setting controller. private static final Map<String, String> sSkinScaling = new HashMap<String, String>(); private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?"); private final AvdInfo mAvd; private final String mSdkLocation; private final SettingsController mSettingsController; private Text mScreenSize; private Text mMonitorDpi; private Button mScaleButton; private float mScale = 0.f; private boolean mWipeData = false; private int mDensity = 160; // medium density private int mSize1 = -1; private int mSize2 = -1; private String mSkinDisplay; private boolean mEnableScaling = true; private Label mScaleField; private boolean mHasSnapshot = true; private boolean mSnapshotSave = true; private boolean mSnapshotLaunch = true; private Button mSnapshotLaunchCheckbox; AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation, SettingsController settingsController) { super(parentShell, 2, false); mAvd = avd; mSdkLocation = sdkLocation; mSettingsController = settingsController; if (mAvd == null) { throw new IllegalArgumentException("avd cannot be null"); } if (mSdkLocation == null) { throw new IllegalArgumentException("sdkLocation cannot be null"); } computeSkinData(); } public boolean hasWipeData() { return mWipeData; } /** * Returns the scaling factor, or 0.f if none are set. */ public float getScale() { return mScale; } @Override public void createDialogContent(final Composite parent) { GridData gd; Label l = new Label(parent, SWT.NONE); l.setText("Skin:"); l = new Label(parent, SWT.NONE); l.setText(mSkinDisplay == null ? "None" : mSkinDisplay); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); l = new Label(parent, SWT.NONE); l.setText("Density:"); l = new Label(parent, SWT.NONE); l.setText(getDensityText()); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mScaleButton = new Button(parent, SWT.CHECK); mScaleButton.setText("Scale display to real size"); mScaleButton.setEnabled(mEnableScaling); boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null; mScaleButton.setSelection(defaultState); mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; final Group scaleGroup = new Group(parent, SWT.NONE); scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalIndent = 30; gd.horizontalSpan = 2; scaleGroup.setLayout(new GridLayout(3, false)); l = new Label(scaleGroup, SWT.NONE); l.setText("Screen Size (in):"); mScreenSize = new Text(scaleGroup, SWT.BORDER); mScreenSize.setText(getScreenSize()); mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mScreenSize.addVerifyListener(new VerifyListener() { public void verifyText(VerifyEvent event) { // combine the current content and the new text String text = mScreenSize.getText(); text = text.substring(0, event.start) + event.text + text.substring(event.end); // now make sure it's a match for the regex event.doit = sScreenSizePattern.matcher(text).matches(); } }); mScreenSize.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) { onScaleChange(); } }); // empty composite, only 2 widgets on this line. new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData()); gd.widthHint = gd.heightHint = 0; l = new Label(scaleGroup, SWT.NONE); l.setText("Monitor dpi:"); mMonitorDpi = new Text(scaleGroup, SWT.BORDER); mMonitorDpi.setText(Integer.toString(getMonitorDpi())); mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.widthHint = 50; mMonitorDpi.addVerifyListener(new VerifyListener() { public void verifyText(VerifyEvent event) { // check for digit only. for (int i = 0 ; i < event.text.length(); i++) { char letter = event.text.charAt(i); if (letter < '0' || letter > '9') { event.doit = false; return; } } } }); mMonitorDpi.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) { onScaleChange(); } }); Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT); button.setText("?"); button.setToolTipText("Click to figure out your monitor's pixel density"); button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell()); if (dialog.open() == Window.OK) { mMonitorDpi.setText(Integer.toString(dialog.getDensity())); } } }); l = new Label(scaleGroup, SWT.NONE); l.setText("Scale:"); mScaleField = new Label(scaleGroup, SWT.NONE); mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true /*grabExcessHorizontalSpace*/, true /*grabExcessVerticalSpace*/, 2 /*horizontalSpan*/, 1 /*verticalSpan*/)); setScale(mScale); // set initial text value enableGroup(scaleGroup, defaultState); mScaleButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean enabled = mScaleButton.getSelection(); enableGroup(scaleGroup, enabled); if (enabled) { onScaleChange(); } else { setScale(0); } } }); final Button wipeButton = new Button(parent, SWT.CHECK); wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; wipeButton.setText("Wipe user data"); wipeButton.setSelection(mWipeData = sWipeData); wipeButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { mWipeData = wipeButton.getSelection(); updateSnapshotLaunchAvailability(); } }); Map<String, String> prop = mAvd.getProperties(); String snapshotPresent = prop.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); mHasSnapshot = (snapshotPresent != null) && snapshotPresent.equals("true"); mSnapshotLaunchCheckbox = new Button(parent, SWT.CHECK); mSnapshotLaunchCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; mSnapshotLaunchCheckbox.setText("Launch from snapshot"); updateSnapshotLaunchAvailability(); mSnapshotLaunchCheckbox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { mSnapshotLaunch = mSnapshotLaunchCheckbox.getSelection(); } }); final Button snapshotSaveCheckbox = new Button(parent, SWT.CHECK); snapshotSaveCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; snapshotSaveCheckbox.setText("Save to snapshot"); snapshotSaveCheckbox.setSelection((mSnapshotSave = sSnapshotSave) && mHasSnapshot); snapshotSaveCheckbox.setEnabled(mHasSnapshot); snapshotSaveCheckbox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { mSnapshotSave = snapshotSaveCheckbox.getSelection(); } }); l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; // if the scaling is enabled by default, we must initialize the value of mScale if (defaultState) { onScaleChange(); } } /** On Windows we need to manually enable/disable the children of a group */ private void enableGroup(final Group group, boolean enabled) { group.setEnabled(enabled); for (Control c : group.getChildren()) { c.setEnabled(enabled); } } @Override protected void configureShell(Shell newShell) { super.configureShell(newShell); newShell.setText("Launch Options"); } @Override protected Button createButton(Composite parent, int id, String label, boolean defaultButton) { if (id == IDialogConstants.OK_ID) { label = "Launch"; } return super.createButton(parent, id, label, defaultButton); } @Override protected void okPressed() { // override ok to store some info // first the monitor dpi String dpi = mMonitorDpi.getText(); if (dpi.length() > 0) { sMonitorDpi = Integer.parseInt(dpi); // if there is a setting controller, save it if (mSettingsController != null) { mSettingsController.setMonitorDensity(sMonitorDpi); mSettingsController.saveSettings(); } } // now the scale factor String key = mAvd.getName(); sSkinScaling.remove(key); if (mScaleButton.getSelection()) { String size = mScreenSize.getText(); if (size.length() > 0) { sSkinScaling.put(key, size); } } // and then the wipe-data checkbox sWipeData = mWipeData; // and the snapshot handling if those checkboxes are enabled. if (mHasSnapshot) { sSnapshotSave = mSnapshotSave; if (!mWipeData) { sSnapshotLaunch = mSnapshotLaunch; } } // finally continue with the ok action super.okPressed(); } private void computeSkinData() { Map<String, String> prop = mAvd.getProperties(); String dpi = prop.get("hw.lcd.density"); if (dpi != null && dpi.length() > 0) { mDensity = Integer.parseInt(dpi); } findSkinResolution(); } private void onScaleChange() { String sizeStr = mScreenSize.getText(); if (sizeStr.length() == 0) { setScale(0); return; } String dpiStr = mMonitorDpi.getText(); if (dpiStr.length() == 0) { setScale(0); return; } int dpi = Integer.parseInt(dpiStr); float size = Float.parseFloat(sizeStr); /* * We are trying to emulate the following device: * resolution: 'mSize1'x'mSize2' * density: 'mDensity' * screen diagonal: 'size' * ontop a monitor running at 'dpi' */ // We start by computing the screen diagonal in pixels, if the density was really mDensity float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2); // Now we would convert this in actual inches: // diagonalIn = diagonal / mDensity // the scale factor is a mix of adapting to the new density and to the new size. // (size/diagonalIn) * (dpi/mDensity) // this can be simplified to: setScale((size * dpi) / diagonalPx); } private void setScale(float scale) { mScale = scale; // Do the rounding exactly like AvdSelector will do. scale = Math.round(scale * 100); scale /= 100.f; if (scale == 0.f) { mScaleField.setText("default"); //$NON-NLS-1$ } else { mScaleField.setText(String.format("%.2f", scale)); //$NON-NLS-1$ } } /** * Returns the monitor dpi to start with. * This can be coming from the settings, the session-based storage, or the from whatever Java * can tell us. */ private int getMonitorDpi() { if (mSettingsController != null) { sMonitorDpi = mSettingsController.getMonitorDensity(); } if (sMonitorDpi == -1) { // first time? try to get a value sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution(); } return sMonitorDpi; } /** * Returns the screen size to start with. * <p/>If an emulator with the same skin was already launched, scaled, the size used is reused. * <p/>Otherwise the default is returned (3) */ private String getScreenSize() { String size = sSkinScaling.get(mAvd.getName()); if (size != null) { return size; } return "3"; } /** * Returns a display string for the density. */ private String getDensityText() { switch (mDensity) { case 120: return "Low (120)"; case 160: return "Medium (160)"; case 240: return "High (240)"; } return Integer.toString(mDensity); } /** * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}. */ private void findSkinResolution() { Map<String, String> prop = mAvd.getProperties(); String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME); if (skinName != null) { Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName); if (m != null && m.matches()) { mSize1 = Integer.parseInt(m.group(1)); mSize2 = Integer.parseInt(m.group(2)); mSkinDisplay = skinName; mEnableScaling = true; return; } } // The resolution is inside the layout file of the skin. mEnableScaling = false; // default to false for now. // path to the skin layout file. String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH); if (skinPath != null) { File skinFolder = new File(mSdkLocation, skinPath); if (skinFolder.isDirectory()) { File layoutFile = new File(skinFolder, "layout"); if (layoutFile.isFile()) { if (parseLayoutFile(layoutFile)) { mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2); mEnableScaling = true; } else { mSkinDisplay = skinName; } } } } } /** * Parses a layout file. * <p/> * the format is relatively easy. It's a collection of items defined as * ≶name> { * ≶content> * } * * content is either 1+ items or 1+ properties * properties are defined as * ≶name>≶whitespace>≶value> * * We're going to look for an item called display, with 2 properties height and width. * This is very basic parser. * * @param layoutFile the file to parse * @return true if both sizes where found. */ private boolean parseLayoutFile(File layoutFile) { try { BufferedReader input = new BufferedReader(new FileReader(layoutFile)); String line; while ((line = input.readLine()) != null) { // trim to remove whitespace line = line.trim(); int len = line.length(); if (len == 0) continue; // check if this is a new item if (line.charAt(len-1) == '{') { // this is the start of a node String[] tokens = line.split(" "); if ("display".equals(tokens[0])) { // this is the one we're looking for! while ((mSize1 == -1 || mSize2 == -1) && (line = input.readLine()) != null) { // trim to remove whitespace line = line.trim(); len = line.length(); if (len == 0) continue; if ("}".equals(line)) { // looks like we're done with the item. break; } tokens = line.split(" "); if (tokens.length >= 2) { // there can be multiple space between the name and value // in which case we'll get an extra empty token in the middle. if ("width".equals(tokens[0])) { mSize1 = Integer.parseInt(tokens[tokens.length-1]); } else if ("height".equals(tokens[0])) { mSize2 = Integer.parseInt(tokens[tokens.length-1]); } } } return mSize1 != -1 && mSize2 != -1; } } } // if it reaches here, display was not found. // false is returned below. } catch (IOException e) { // ignore. } return false; } /** * @return Whether there's a snapshot file available. */ public boolean hasSnapshot() { return mHasSnapshot; } /** * @return Whether to launch and load snapshot. */ public boolean hasSnapshotLaunch() { return mSnapshotLaunch && !hasWipeData(); } /** * @return Whether to preserve emulator state to snapshot. */ public boolean hasSnapshotSave() { return mSnapshotSave; } /** * Updates snapshot launch availability, for when mWipeData value changes. */ private void updateSnapshotLaunchAvailability() { boolean enabled = !mWipeData && mHasSnapshot; mSnapshotLaunchCheckbox.setEnabled(enabled); mSnapshotLaunch = enabled && sSnapshotLaunch; mSnapshotLaunchCheckbox.setSelection(mSnapshotLaunch); } }