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 @Override 157 public int compare(IAndroidTarget o1, IAndroidTarget o2) { 158 return o1.compareTo(o2); 159 } 160 }); 161 } 162 163 fillTable(mTable); 164 } 165 166 /** 167 * Sets a selection listener. Set it to null to remove it. 168 * The listener will be called <em>after</em> this table processed its selection 169 * events so that the caller can see the updated state. 170 * <p/> 171 * The event's item contains a {@link TableItem}. 172 * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. 173 * <p/> 174 * It is recommended that the caller uses the {@link #getSelected()} method instead. 175 * 176 * @param selectionListener The new listener or null to remove it. 177 */ 178 public void setSelectionListener(SelectionListener selectionListener) { 179 mSelectionListener = selectionListener; 180 } 181 182 /** 183 * Sets the current target selection. 184 * <p/> 185 * If the selection is actually changed, this will invoke the selection listener 186 * (if any) with a null event. 187 * 188 * @param target the target to be selection 189 * @return true if the target could be selected, false otherwise. 190 */ 191 public boolean setSelection(IAndroidTarget target) { 192 if (!mAllowSelection) { 193 return false; 194 } 195 196 boolean found = false; 197 boolean modified = false; 198 199 if (mTable != null && !mTable.isDisposed()) { 200 for (TableItem i : mTable.getItems()) { 201 if ((IAndroidTarget) i.getData() == target) { 202 found = true; 203 if (!i.getChecked()) { 204 modified = true; 205 i.setChecked(true); 206 } 207 } else if (i.getChecked()) { 208 modified = true; 209 i.setChecked(false); 210 } 211 } 212 } 213 214 if (modified && mSelectionListener != null) { 215 mSelectionListener.widgetSelected(null); 216 } 217 218 return found; 219 } 220 221 /** 222 * Returns the selected item. 223 * 224 * @return The selected item or null. 225 */ 226 public IAndroidTarget getSelected() { 227 if (mTable == null || mTable.isDisposed()) { 228 return null; 229 } 230 231 for (TableItem i : mTable.getItems()) { 232 if (i.getChecked()) { 233 return (IAndroidTarget) i.getData(); 234 } 235 } 236 return null; 237 } 238 239 /** 240 * Adds a listener to adjust the columns width when the parent is resized. 241 * <p/> 242 * If we need something more fancy, we might want to use this: 243 * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co 244 */ 245 private void adjustColumnsWidth(final Table table, 246 final TableColumn column0, 247 final TableColumn column1, 248 final TableColumn column2, 249 final TableColumn column3) { 250 // Add a listener to resize the column to the full width of the table 251 table.addControlListener(new ControlAdapter() { 252 @Override 253 public void controlResized(ControlEvent e) { 254 Rectangle r = table.getClientArea(); 255 int width = r.width; 256 257 // On the Mac, the width of the checkbox column is not included (and checkboxes 258 // are shown if mAllowSelection=true). Subtract this size from the available 259 // width to be distributed among the columns. 260 if (mAllowSelection 261 && SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { 262 width -= getCheckboxWidth(); 263 } 264 265 column0.setWidth(width * 30 / 100); // 30% 266 column1.setWidth(width * 45 / 100); // 45% 267 column2.setWidth(width * 15 / 100); // 15% 268 column3.setWidth(width * 10 / 100); // 10% 269 } 270 }); 271 } 272 273 274 /** 275 * Creates a selection listener that will check or uncheck the whole line when 276 * double-clicked (aka "the default selection"). 277 */ 278 private void setupSelectionListener(final Table table) { 279 if (!mAllowSelection) { 280 return; 281 } 282 283 // Add a selection listener that will check/uncheck items when they are double-clicked 284 table.addSelectionListener(new SelectionListener() { 285 /** Default selection means double-click on "most" platforms */ 286 @Override 287 public void widgetDefaultSelected(SelectionEvent e) { 288 if (e.item instanceof TableItem) { 289 TableItem i = (TableItem) e.item; 290 i.setChecked(!i.getChecked()); 291 enforceSingleSelection(i); 292 updateDescription(i); 293 } 294 295 if (mSelectionListener != null) { 296 mSelectionListener.widgetDefaultSelected(e); 297 } 298 } 299 300 @Override 301 public void widgetSelected(SelectionEvent e) { 302 if (e.item instanceof TableItem) { 303 TableItem i = (TableItem) e.item; 304 enforceSingleSelection(i); 305 updateDescription(i); 306 } 307 308 if (mSelectionListener != null) { 309 mSelectionListener.widgetSelected(e); 310 } 311 } 312 313 /** 314 * If we're not in multiple selection mode, uncheck all other 315 * items when this one is selected. 316 */ 317 private void enforceSingleSelection(TableItem item) { 318 if (item.getChecked()) { 319 Table parentTable = item.getParent(); 320 for (TableItem i2 : parentTable.getItems()) { 321 if (i2 != item && i2.getChecked()) { 322 i2.setChecked(false); 323 } 324 } 325 } 326 } 327 }); 328 } 329 330 331 /** 332 * Fills the table with all SDK targets. 333 * The table columns are: 334 * <ul> 335 * <li>column 0: sdk name 336 * <li>column 1: sdk vendor 337 * <li>column 2: sdk api name 338 * <li>column 3: sdk version 339 * </ul> 340 */ 341 private void fillTable(final Table table) { 342 343 if (table == null || table.isDisposed()) { 344 return; 345 } 346 347 table.removeAll(); 348 349 if (mTargets != null && mTargets.length > 0) { 350 table.setEnabled(true); 351 for (IAndroidTarget target : mTargets) { 352 TableItem item = new TableItem(table, SWT.NONE); 353 item.setData(target); 354 item.setText(0, target.getName()); 355 item.setText(1, target.getVendor()); 356 item.setText(2, target.getVersionName()); 357 item.setText(3, target.getVersion().getApiString()); 358 } 359 } else { 360 table.setEnabled(false); 361 TableItem item = new TableItem(table, SWT.NONE); 362 item.setData(null); 363 item.setText(0, "--"); 364 item.setText(1, "No target available"); 365 item.setText(2, "--"); 366 item.setText(3, "--"); 367 } 368 } 369 370 /** 371 * Sets up a tooltip that displays the current item description. 372 * <p/> 373 * Displaying a tooltip over the table looks kind of odd here. Instead we actually 374 * display the description in a label under the table. 375 */ 376 private void setupTooltip(final Table table) { 377 378 if (table == null || table.isDisposed()) { 379 return; 380 } 381 382 /* 383 * Reference: 384 * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup 385 */ 386 387 final Listener listener = new Listener() { 388 @Override 389 public void handleEvent(Event event) { 390 391 switch(event.type) { 392 case SWT.KeyDown: 393 case SWT.MouseExit: 394 case SWT.MouseDown: 395 return; 396 397 case SWT.MouseHover: 398 updateDescription(table.getItem(new Point(event.x, event.y))); 399 break; 400 401 case SWT.Selection: 402 if (event.item instanceof TableItem) { 403 updateDescription((TableItem) event.item); 404 } 405 break; 406 407 default: 408 return; 409 } 410 411 } 412 }; 413 414 table.addListener(SWT.Dispose, listener); 415 table.addListener(SWT.KeyDown, listener); 416 table.addListener(SWT.MouseMove, listener); 417 table.addListener(SWT.MouseHover, listener); 418 } 419 420 /** 421 * Updates the description label with the description of the item's android target, if any. 422 */ 423 private void updateDescription(TableItem item) { 424 if (item != null) { 425 Object data = item.getData(); 426 if (data instanceof IAndroidTarget) { 427 String newTooltip = ((IAndroidTarget) data).getDescription(); 428 mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ 429 } 430 } 431 } 432 433 /** Enables or disables the controls. */ 434 public void setEnabled(boolean enabled) { 435 if (mInnerGroup != null && mTable != null && !mTable.isDisposed()) { 436 enableControl(mInnerGroup, enabled); 437 } 438 } 439 440 /** Enables or disables controls; recursive for composite controls. */ 441 private void enableControl(Control c, boolean enabled) { 442 c.setEnabled(enabled); 443 if (c instanceof Composite) 444 for (Control c2 : ((Composite) c).getChildren()) { 445 enableControl(c2, enabled); 446 } 447 } 448 449 /** Computes the width of a checkbox */ 450 private int getCheckboxWidth() { 451 if (sCheckboxWidth == -1) { 452 Shell shell = new Shell(mTable.getShell(), SWT.NO_TRIM); 453 Button checkBox = new Button(shell, SWT.CHECK); 454 sCheckboxWidth = checkBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; 455 shell.dispose(); 456 } 457 458 return sCheckboxWidth; 459 } 460 } 461