Home | History | Annotate | Download | only in widgets
      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