1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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.sdkuilib.internal.widgets; 18 19 import com.android.sdklib.IAndroidTarget; 20 import com.android.sdklib.SdkConstants; 21 22 import org.eclipse.swt.SWT; 23 import org.eclipse.swt.events.ControlAdapter; 24 import org.eclipse.swt.events.ControlEvent; 25 import org.eclipse.swt.events.SelectionEvent; 26 import org.eclipse.swt.events.SelectionListener; 27 import org.eclipse.swt.graphics.Point; 28 import org.eclipse.swt.graphics.Rectangle; 29 import org.eclipse.swt.layout.GridData; 30 import org.eclipse.swt.layout.GridLayout; 31 import org.eclipse.swt.widgets.Button; 32 import org.eclipse.swt.widgets.Composite; 33 import org.eclipse.swt.widgets.Control; 34 import org.eclipse.swt.widgets.Event; 35 import org.eclipse.swt.widgets.Label; 36 import org.eclipse.swt.widgets.Listener; 37 import org.eclipse.swt.widgets.Shell; 38 import org.eclipse.swt.widgets.Table; 39 import org.eclipse.swt.widgets.TableColumn; 40 import org.eclipse.swt.widgets.TableItem; 41 42 import java.util.Arrays; 43 import java.util.Comparator; 44 45 46 /** 47 * The SDK target selector is a table that is added to the given parent composite. 48 * <p/> 49 * To use, create it using {@link #SdkTargetSelector(Composite, IAndroidTarget[], boolean)} then 50 * call {@link #setSelection(IAndroidTarget)}, {@link #setSelectionListener(SelectionListener)} 51 * and finally use {@link #getSelected()} to retrieve the 52 * selection. 53 */ 54 public class SdkTargetSelector { 55 56 private IAndroidTarget[] mTargets; 57 private final boolean mAllowSelection; 58 private SelectionListener mSelectionListener; 59 private Table mTable; 60 private Label mDescription; 61 private Composite mInnerGroup; 62 63 /** Cache for {@link #getCheckboxWidth()} */ 64 private static int sCheckboxWidth = -1; 65 66 /** 67 * Creates a new SDK Target Selector. 68 * 69 * @param parent The parent composite where the selector will be added. 70 * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify. 71 * Targets can be null or an empty array, in which case the table is disabled. 72 */ 73 public SdkTargetSelector(Composite parent, IAndroidTarget[] targets) { 74 this(parent, targets, true /*allowSelection*/); 75 } 76 77 /** 78 * Creates a new SDK Target Selector. 79 * 80 * @param parent The parent composite where the selector will be added. 81 * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify. 82 * Targets can be null or an empty array, in which case the table is disabled. 83 * @param allowSelection True if selection is enabled. 84 */ 85 public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, boolean allowSelection) { 86 // Layout has 1 column 87 mInnerGroup = new Composite(parent, SWT.NONE); 88 mInnerGroup.setLayout(new GridLayout()); 89 mInnerGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); 90 mInnerGroup.setFont(parent.getFont()); 91 92 mAllowSelection = allowSelection; 93 int style = SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION; 94 if (allowSelection) { 95 style |= SWT.CHECK; 96 } 97 mTable = new Table(mInnerGroup, style); 98 mTable.setHeaderVisible(true); 99 mTable.setLinesVisible(false); 100 101 GridData data = new GridData(); 102 data.grabExcessVerticalSpace = true; 103 data.grabExcessHorizontalSpace = true; 104 data.horizontalAlignment = GridData.FILL; 105 data.verticalAlignment = GridData.FILL; 106 mTable.setLayoutData(data); 107 108 mDescription = new Label(mInnerGroup, SWT.WRAP); 109 mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 110 111 // create the table columns 112 final TableColumn column0 = new TableColumn(mTable, SWT.NONE); 113 column0.setText("Target Name"); 114 final TableColumn column1 = new TableColumn(mTable, SWT.NONE); 115 column1.setText("Vendor"); 116 final TableColumn column2 = new TableColumn(mTable, SWT.NONE); 117 column2.setText("Platform"); 118 final TableColumn column3 = new TableColumn(mTable, SWT.NONE); 119 column3.setText("API Level"); 120 121 adjustColumnsWidth(mTable, column0, column1, column2, column3); 122 setupSelectionListener(mTable); 123 setTargets(targets); 124 setupTooltip(mTable); 125 } 126 127 /** 128 * Returns the layout data of the inner composite widget that contains the target selector. 129 * By default the layout data is set to a {@link GridData} with a {@link GridData#FILL_BOTH} 130 * mode. 131 * <p/> 132 * This can be useful if you want to change the {@link GridData#horizontalSpan} for example. 133 */ 134 public Object getLayoutData() { 135 return mInnerGroup.getLayoutData(); 136 } 137 138 /** 139 * Returns the list of known targets. 140 * <p/> 141 * This is not a copy. Callers must <em>not</em> modify this array. 142 */ 143 public IAndroidTarget[] getTargets() { 144 return mTargets; 145 } 146 147 /** 148 * Changes the targets of the SDK Target Selector. 149 * 150 * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify. 151 */ 152 public void setTargets(IAndroidTarget[] targets) { 153 mTargets = targets; 154 if (mTargets != null) { 155 Arrays.sort(mTargets, new Comparator<IAndroidTarget>() { 156 public int compare(IAndroidTarget o1, IAndroidTarget o2) { 157 return o1.compareTo(o2); 158 } 159 }); 160 } 161 162 fillTable(mTable); 163 } 164 165 /** 166 * Sets a selection listener. Set it to null to remove it. 167 * The listener will be called <em>after</em> this table processed its selection 168 * events so that the caller can see the updated state. 169 * <p/> 170 * The event's item contains a {@link TableItem}. 171 * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. 172 * <p/> 173 * It is recommended that the caller uses the {@link #getSelected()} method instead. 174 * 175 * @param selectionListener The new listener or null to remove it. 176 */ 177 public void setSelectionListener(SelectionListener selectionListener) { 178 mSelectionListener = selectionListener; 179 } 180 181 /** 182 * Sets the current target selection. 183 * <p/> 184 * If the selection is actually changed, this will invoke the selection listener 185 * (if any) with a null event. 186 * 187 * @param target the target to be selection 188 * @return true if the target could be selected, false otherwise. 189 */ 190 public boolean setSelection(IAndroidTarget target) { 191 if (!mAllowSelection) { 192 return false; 193 } 194 195 boolean found = false; 196 boolean modified = false; 197 198 if (mTable != null && !mTable.isDisposed()) { 199 for (TableItem i : mTable.getItems()) { 200 if ((IAndroidTarget) i.getData() == target) { 201 found = true; 202 if (!i.getChecked()) { 203 modified = true; 204 i.setChecked(true); 205 } 206 } else if (i.getChecked()) { 207 modified = true; 208 i.setChecked(false); 209 } 210 } 211 } 212 213 if (modified && mSelectionListener != null) { 214 mSelectionListener.widgetSelected(null); 215 } 216 217 return found; 218 } 219 220 /** 221 * Returns the selected item. 222 * 223 * @return The selected item or null. 224 */ 225 public IAndroidTarget getSelected() { 226 if (mTable == null || mTable.isDisposed()) { 227 return null; 228 } 229 230 for (TableItem i : mTable.getItems()) { 231 if (i.getChecked()) { 232 return (IAndroidTarget) i.getData(); 233 } 234 } 235 return null; 236 } 237 238 /** 239 * Adds a listener to adjust the columns width when the parent is resized. 240 * <p/> 241 * If we need something more fancy, we might want to use this: 242 * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co 243 */ 244 private void adjustColumnsWidth(final Table table, 245 final TableColumn column0, 246 final TableColumn column1, 247 final TableColumn column2, 248 final TableColumn column3) { 249 // Add a listener to resize the column to the full width of the table 250 table.addControlListener(new ControlAdapter() { 251 @Override 252 public void controlResized(ControlEvent e) { 253 Rectangle r = table.getClientArea(); 254 int width = r.width; 255 256 // On the Mac, the width of the checkbox column is not included (and checkboxes 257 // are shown if mAllowSelection=true). Subtract this size from the available 258 // width to be distributed among the columns. 259 if (mAllowSelection 260 && SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { 261 width -= getCheckboxWidth(); 262 } 263 264 column0.setWidth(width * 30 / 100); // 30% 265 column1.setWidth(width * 45 / 100); // 45% 266 column2.setWidth(width * 15 / 100); // 15% 267 column3.setWidth(width * 10 / 100); // 10% 268 } 269 }); 270 } 271 272 273 /** 274 * Creates a selection listener that will check or uncheck the whole line when 275 * double-clicked (aka "the default selection"). 276 */ 277 private void setupSelectionListener(final Table table) { 278 if (!mAllowSelection) { 279 return; 280 } 281 282 // Add a selection listener that will check/uncheck items when they are double-clicked 283 table.addSelectionListener(new SelectionListener() { 284 /** Default selection means double-click on "most" platforms */ 285 public void widgetDefaultSelected(SelectionEvent e) { 286 if (e.item instanceof TableItem) { 287 TableItem i = (TableItem) e.item; 288 i.setChecked(!i.getChecked()); 289 enforceSingleSelection(i); 290 updateDescription(i); 291 } 292 293 if (mSelectionListener != null) { 294 mSelectionListener.widgetDefaultSelected(e); 295 } 296 } 297 298 public void widgetSelected(SelectionEvent e) { 299 if (e.item instanceof TableItem) { 300 TableItem i = (TableItem) e.item; 301 enforceSingleSelection(i); 302 updateDescription(i); 303 } 304 305 if (mSelectionListener != null) { 306 mSelectionListener.widgetSelected(e); 307 } 308 } 309 310 /** 311 * If we're not in multiple selection mode, uncheck all other 312 * items when this one is selected. 313 */ 314 private void enforceSingleSelection(TableItem item) { 315 if (item.getChecked()) { 316 Table parentTable = item.getParent(); 317 for (TableItem i2 : parentTable.getItems()) { 318 if (i2 != item && i2.getChecked()) { 319 i2.setChecked(false); 320 } 321 } 322 } 323 } 324 }); 325 } 326 327 328 /** 329 * Fills the table with all SDK targets. 330 * The table columns are: 331 * <ul> 332 * <li>column 0: sdk name 333 * <li>column 1: sdk vendor 334 * <li>column 2: sdk api name 335 * <li>column 3: sdk version 336 * </ul> 337 */ 338 private void fillTable(final Table table) { 339 340 if (table == null || table.isDisposed()) { 341 return; 342 } 343 344 table.removeAll(); 345 346 if (mTargets != null && mTargets.length > 0) { 347 table.setEnabled(true); 348 for (IAndroidTarget target : mTargets) { 349 TableItem item = new TableItem(table, SWT.NONE); 350 item.setData(target); 351 item.setText(0, target.getName()); 352 item.setText(1, target.getVendor()); 353 item.setText(2, target.getVersionName()); 354 item.setText(3, target.getVersion().getApiString()); 355 } 356 } else { 357 table.setEnabled(false); 358 TableItem item = new TableItem(table, SWT.NONE); 359 item.setData(null); 360 item.setText(0, "--"); 361 item.setText(1, "No target available"); 362 item.setText(2, "--"); 363 item.setText(3, "--"); 364 } 365 } 366 367 /** 368 * Sets up a tooltip that displays the current item description. 369 * <p/> 370 * Displaying a tooltip over the table looks kind of odd here. Instead we actually 371 * display the description in a label under the table. 372 */ 373 private void setupTooltip(final Table table) { 374 375 if (table == null || table.isDisposed()) { 376 return; 377 } 378 379 /* 380 * Reference: 381 * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup 382 */ 383 384 final Listener listener = new Listener() { 385 public void handleEvent(Event event) { 386 387 switch(event.type) { 388 case SWT.KeyDown: 389 case SWT.MouseExit: 390 case SWT.MouseDown: 391 return; 392 393 case SWT.MouseHover: 394 updateDescription(table.getItem(new Point(event.x, event.y))); 395 break; 396 397 case SWT.Selection: 398 if (event.item instanceof TableItem) { 399 updateDescription((TableItem) event.item); 400 } 401 break; 402 403 default: 404 return; 405 } 406 407 } 408 }; 409 410 table.addListener(SWT.Dispose, listener); 411 table.addListener(SWT.KeyDown, listener); 412 table.addListener(SWT.MouseMove, listener); 413 table.addListener(SWT.MouseHover, listener); 414 } 415 416 /** 417 * Updates the description label with the description of the item's android target, if any. 418 */ 419 private void updateDescription(TableItem item) { 420 if (item != null) { 421 Object data = item.getData(); 422 if (data instanceof IAndroidTarget) { 423 String newTooltip = ((IAndroidTarget) data).getDescription(); 424 mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ 425 } 426 } 427 } 428 429 /** Enables or disables the controls. */ 430 public void setEnabled(boolean enabled) { 431 if (mInnerGroup != null && mTable != null && !mTable.isDisposed()) { 432 enableControl(mInnerGroup, enabled); 433 } 434 } 435 436 /** Enables or disables controls; recursive for composite controls. */ 437 private void enableControl(Control c, boolean enabled) { 438 c.setEnabled(enabled); 439 if (c instanceof Composite) 440 for (Control c2 : ((Composite) c).getChildren()) { 441 enableControl(c2, enabled); 442 } 443 } 444 445 /** Computes the width of a checkbox */ 446 private int getCheckboxWidth() { 447 if (sCheckboxWidth == -1) { 448 Shell shell = new Shell(mTable.getShell(), SWT.NO_TRIM); 449 Button checkBox = new Button(shell, SWT.CHECK); 450 sCheckboxWidth = checkBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; 451 shell.dispose(); 452 } 453 454 return sCheckboxWidth; 455 } 456 } 457