OSDN Git Service

3c1ba8a53e337f208cbb50481d8342aef5c71b3d
[android-x86/sdk.git] / sdkmanager / libs / sdkuilib / src / com / android / sdkuilib / internal / repository / sdkman2 / PackagesPage.java
1 /*
2  * Copyright (C) 2011 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.repository.sdkman2;
18
19 import com.android.sdklib.SdkConstants;
20 import com.android.sdklib.internal.repository.Archive;
21 import com.android.sdklib.internal.repository.IDescription;
22 import com.android.sdklib.internal.repository.ITask;
23 import com.android.sdklib.internal.repository.ITaskMonitor;
24 import com.android.sdklib.internal.repository.Package;
25 import com.android.sdklib.internal.repository.SdkSource;
26 import com.android.sdkuilib.internal.repository.IPageListener;
27 import com.android.sdkuilib.internal.repository.UpdaterData;
28 import com.android.sdkuilib.internal.repository.UpdaterPage;
29 import com.android.sdkuilib.internal.repository.icons.ImageFactory;
30 import com.android.sdkuilib.internal.repository.sdkman2.PackageLoader.ISourceLoadedCallback;
31 import com.android.sdkuilib.internal.repository.sdkman2.PkgItem.PkgState;
32 import com.android.sdkuilib.repository.ISdkChangeListener;
33 import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext;
34 import com.android.sdkuilib.ui.GridDataBuilder;
35 import com.android.sdkuilib.ui.GridLayoutBuilder;
36
37 import org.eclipse.jface.dialogs.MessageDialog;
38 import org.eclipse.jface.viewers.CellLabelProvider;
39 import org.eclipse.jface.viewers.CheckStateChangedEvent;
40 import org.eclipse.jface.viewers.CheckboxTreeViewer;
41 import org.eclipse.jface.viewers.ColumnLabelProvider;
42 import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
43 import org.eclipse.jface.viewers.DoubleClickEvent;
44 import org.eclipse.jface.viewers.ICheckStateListener;
45 import org.eclipse.jface.viewers.IDoubleClickListener;
46 import org.eclipse.jface.viewers.ISelection;
47 import org.eclipse.jface.viewers.ITableFontProvider;
48 import org.eclipse.jface.viewers.ITreeContentProvider;
49 import org.eclipse.jface.viewers.ITreeSelection;
50 import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider;
51 import org.eclipse.jface.viewers.TreePath;
52 import org.eclipse.jface.viewers.TreeViewerColumn;
53 import org.eclipse.jface.viewers.Viewer;
54 import org.eclipse.jface.viewers.ViewerFilter;
55 import org.eclipse.jface.window.ToolTip;
56 import org.eclipse.swt.SWT;
57 import org.eclipse.swt.events.DisposeEvent;
58 import org.eclipse.swt.events.DisposeListener;
59 import org.eclipse.swt.events.SelectionAdapter;
60 import org.eclipse.swt.events.SelectionEvent;
61 import org.eclipse.swt.graphics.Color;
62 import org.eclipse.swt.graphics.Font;
63 import org.eclipse.swt.graphics.FontData;
64 import org.eclipse.swt.graphics.Image;
65 import org.eclipse.swt.graphics.Point;
66 import org.eclipse.swt.widgets.Button;
67 import org.eclipse.swt.widgets.Composite;
68 import org.eclipse.swt.widgets.Control;
69 import org.eclipse.swt.widgets.Event;
70 import org.eclipse.swt.widgets.Group;
71 import org.eclipse.swt.widgets.Label;
72 import org.eclipse.swt.widgets.Link;
73 import org.eclipse.swt.widgets.MenuItem;
74 import org.eclipse.swt.widgets.Text;
75 import org.eclipse.swt.widgets.Tree;
76 import org.eclipse.swt.widgets.TreeColumn;
77
78 import java.io.File;
79 import java.net.MalformedURLException;
80 import java.net.URL;
81 import java.util.ArrayList;
82 import java.util.HashMap;
83 import java.util.List;
84 import java.util.Map;
85 import java.util.Map.Entry;
86
87 /**
88  * Page that displays both locally installed packages as well as all known
89  * remote available packages. This gives an overview of what is installed
90  * vs what is available and allows the user to update or install packages.
91  */
92 public class PackagesPage extends UpdaterPage
93         implements ISdkChangeListener, IPageListener {
94
95     static final String ICON_CAT_OTHER      = "pkgcat_other_16.png";    //$NON-NLS-1$
96     static final String ICON_CAT_PLATFORM   = "pkgcat_16.png";          //$NON-NLS-1$
97     static final String ICON_SORT_BY_SOURCE = "source_icon16.png";      //$NON-NLS-1$
98     static final String ICON_SORT_BY_API    = "platform_pkg_16.png";    //$NON-NLS-1$
99     static final String ICON_PKG_NEW        = "pkg_new_16.png";         //$NON-NLS-1$
100     static final String ICON_PKG_UPDATE     = "pkg_update_16.png";      //$NON-NLS-1$
101     static final String ICON_PKG_INSTALLED  = "pkg_installed_16.png";   //$NON-NLS-1$
102
103     enum MenuAction {
104         RELOAD                      (SWT.NONE,  "Reload"),
105         SHOW_ADDON_SITES            (SWT.NONE,  "Manage Add-on Sites..."),
106         TOGGLE_SHOW_ARCHIVES        (SWT.CHECK, "Show Archives Details"),
107         TOGGLE_SHOW_INSTALLED_PKG   (SWT.CHECK, "Show Installed Packages"),
108         TOGGLE_SHOW_OBSOLETE_PKG    (SWT.CHECK, "Show Obsolete Packages"),
109         TOGGLE_SHOW_UPDATE_NEW_PKG  (SWT.CHECK, "Show Updates/New Packages"),
110         SORT_API_LEVEL              (SWT.RADIO, "Sort by API Level"),
111         SORT_SOURCE                 (SWT.RADIO, "Sort by Repository")
112         ;
113
114         private final int mMenuStyle;
115         private final String mMenuTitle;
116
117         MenuAction(int menuStyle, String menuTitle) {
118             mMenuStyle = menuStyle;
119             mMenuTitle = menuTitle;
120         }
121
122         public int getMenuStyle() {
123             return mMenuStyle;
124         }
125
126         public String getMenuTitle() {
127             return mMenuTitle;
128         }
129     };
130
131     private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>();
132
133     private final SdkInvocationContext mContext;
134     private final UpdaterData mUpdaterData;
135     private final PackagesDiffLogic mDiffLogic;
136     private boolean mDisplayArchives = false;
137     private boolean mOperationPending;
138
139     private Text mTextSdkOsPath;
140     private Button mCheckSortSource;
141     private Button mCheckSortApi;
142     private Button mCheckFilterObsolete;
143     private Button mCheckFilterInstalled;
144     private Button mCheckFilterNew;
145     private Composite mGroupOptions;
146     private Composite mGroupSdk;
147     private Group mGroupPackages;
148     private Button mButtonDelete;
149     private Button mButtonInstall;
150     private Tree mTree;
151     private CheckboxTreeViewer mTreeViewer;
152     private TreeViewerColumn mColumnName;
153     private TreeViewerColumn mColumnApi;
154     private TreeViewerColumn mColumnRevision;
155     private TreeViewerColumn mColumnStatus;
156     private Font mTreeFontItalic;
157     private TreeColumn mTreeColumnName;
158
159     public PackagesPage(
160             Composite parent,
161             int swtStyle,
162             UpdaterData updaterData,
163             SdkInvocationContext context) {
164         super(parent, swtStyle);
165         mUpdaterData = updaterData;
166         mContext = context;
167
168         mDiffLogic = new PackagesDiffLogic(updaterData);
169
170         createContents(this);
171         postCreate();  //$hide$
172     }
173
174     public void onPageSelected() {
175         List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
176         if (cats == null || cats.isEmpty()) {
177             // Initialize the package list the first time the page is shown.
178             loadPackages();
179         }
180     }
181
182     @SuppressWarnings("unused")
183     private void createContents(Composite parent) {
184         GridLayoutBuilder.create(parent).noMargins().columns(2);
185
186         mGroupSdk = new Composite(parent, SWT.NONE);
187         GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2);
188         GridLayoutBuilder.create(mGroupSdk).columns(2);
189
190         Label label1 = new Label(mGroupSdk, SWT.NONE);
191         label1.setText("SDK Path:");
192
193         mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE);
194         GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab();
195         mTextSdkOsPath.setEnabled(false);
196
197         mGroupPackages = new Group(parent, SWT.NONE);
198         GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2);
199         mGroupPackages.setText("Packages");
200         GridLayoutBuilder.create(mGroupPackages).columns(1);
201
202         mTreeViewer = new CheckboxTreeViewer(mGroupPackages, SWT.BORDER);
203         mTreeViewer.addFilter(new ViewerFilter() {
204             @Override
205             public boolean select(Viewer viewer, Object parentElement, Object element) {
206                 return filterViewerItem(element);
207             }
208         });
209
210         mTreeViewer.addCheckStateListener(new ICheckStateListener() {
211             public void checkStateChanged(CheckStateChangedEvent event) {
212                 onTreeCheckStateChanged(event); //$hide$
213             }
214         });
215
216         mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
217             public void doubleClick(DoubleClickEvent event) {
218                 onTreeDoubleClick(event); //$hide$
219             }
220         });
221
222         mTree = mTreeViewer.getTree();
223         mTree.setLinesVisible(true);
224         mTree.setHeaderVisible(true);
225         GridDataBuilder.create(mTree).fill().grab();
226
227         // column name icon is set when loading depending on the current filter type
228         // (e.g. API level or source)
229         mColumnName = new TreeViewerColumn(mTreeViewer, SWT.NONE);
230         mTreeColumnName = mColumnName.getColumn();
231         mTreeColumnName.setText("Name");
232         mTreeColumnName.setWidth(340);
233
234         mColumnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE);
235         TreeColumn treeColumn2 = mColumnApi.getColumn();
236         treeColumn2.setText("API");
237         treeColumn2.setAlignment(SWT.CENTER);
238         treeColumn2.setWidth(50);
239
240         mColumnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE);
241         TreeColumn treeColumn3 = mColumnRevision.getColumn();
242         treeColumn3.setText("Rev.");
243         treeColumn3.setToolTipText("Revision currently installed");
244         treeColumn3.setAlignment(SWT.CENTER);
245         treeColumn3.setWidth(50);
246
247
248         mColumnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE);
249         TreeColumn treeColumn4 = mColumnStatus.getColumn();
250         treeColumn4.setText("Status");
251         treeColumn4.setAlignment(SWT.LEAD);
252         treeColumn4.setWidth(190);
253
254         mGroupOptions = new Composite(mGroupPackages, SWT.NONE);
255         GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab();
256         GridLayoutBuilder.create(mGroupOptions).columns(6).noMargins();
257
258         // Options line 1, 6 columns
259
260         Label label3 = new Label(mGroupOptions, SWT.NONE);
261         label3.setText("Show:");
262
263         mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK);
264         mCheckFilterNew.setText("Updates/New");
265         mCheckFilterNew.setToolTipText("Show Updates and New");
266         mCheckFilterNew.addSelectionListener(new SelectionAdapter() {
267             @Override
268             public void widgetSelected(SelectionEvent e) {
269                 refreshViewerInput();
270             }
271         });
272         mCheckFilterNew.setSelection(true);
273
274         mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK);
275         mCheckFilterInstalled.setToolTipText("Show Installed");
276         mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() {
277             @Override
278             public void widgetSelected(SelectionEvent e) {
279                 refreshViewerInput();
280             }
281         });
282         mCheckFilterInstalled.setSelection(true);
283         mCheckFilterInstalled.setText("Installed");
284
285         mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK);
286         mCheckFilterObsolete.setText("Obsolete");
287         mCheckFilterObsolete.setToolTipText("Also show obsolete packages");
288         mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() {
289             @Override
290             public void widgetSelected(SelectionEvent e) {
291                 refreshViewerInput();
292             }
293         });
294         mCheckFilterObsolete.setSelection(false);
295
296         Link linkSelectNew = new Link(mGroupOptions, SWT.NONE);
297         // Note for i18n: we need to identify which link is used, and this is done by using the
298         // text itself so for translation purposes we want to keep the <a> link strings separate.
299         final String strLinkNew = "New";
300         final String strLinkUpdates = "Updates";
301         linkSelectNew.setText(
302                 String.format("Select <a>%1$s</a> or <a>%2$s</a>", strLinkNew, strLinkUpdates));
303         linkSelectNew.setToolTipText("Selects all items that are either new or updates.");
304         GridDataBuilder.create(linkSelectNew).hFill().hGrab();
305         linkSelectNew.addSelectionListener(new SelectionAdapter() {
306             @Override
307             public void widgetSelected(SelectionEvent e) {
308                 super.widgetSelected(e);
309                 boolean selectNew = e.text == null || e.text.equals(strLinkNew);
310                 onSelectNewUpdates(selectNew, !selectNew, false/*selectTop*/);
311             }
312         });
313
314         mButtonInstall = new Button(mGroupOptions, SWT.NONE);
315         mButtonInstall.setText("");  //$NON-NLS-1$  placeholder, filled in updateButtonsState()
316         mButtonInstall.setToolTipText("Install one or more packages");
317         GridDataBuilder.create(mButtonInstall).hFill().vCenter().hGrab();
318         mButtonInstall.addSelectionListener(new SelectionAdapter() {
319             @Override
320             public void widgetSelected(SelectionEvent e) {
321                 onButtonInstall();  //$hide$
322             }
323         });
324
325         // Options line 2, 6 columns
326
327         Label label2 = new Label(mGroupOptions, SWT.NONE);
328         label2.setText("Sort by:");
329
330         mCheckSortApi = new Button(mGroupOptions, SWT.RADIO);
331         mCheckSortApi.setToolTipText("Sort by API level");
332         mCheckSortApi.addSelectionListener(new SelectionAdapter() {
333             @Override
334             public void widgetSelected(SelectionEvent e) {
335                 if (mCheckSortApi.getSelection()) {
336                     refreshViewerInput();
337                     copySelection(true /*toApi*/);
338                     syncViewerSelection();
339                 }
340             }
341         });
342         mCheckSortApi.setText("API level");
343         mCheckSortApi.setSelection(true);
344
345         mCheckSortSource = new Button(mGroupOptions, SWT.RADIO);
346         mCheckSortSource.setText("Repository");
347         mCheckSortSource.setToolTipText("Sort by Repository");
348         mCheckSortSource.addSelectionListener(new SelectionAdapter() {
349             @Override
350             public void widgetSelected(SelectionEvent e) {
351                 if (mCheckSortSource.getSelection()) {
352                     refreshViewerInput();
353                     copySelection(false /*toApi*/);
354                     syncViewerSelection();
355                 }
356             }
357         });
358
359         new Label(mGroupOptions, SWT.NONE);
360
361         Link linkDeselect = new Link(mGroupOptions, SWT.NONE);
362         linkDeselect.setText("<a>Deselect All</a>");
363         linkDeselect.setToolTipText("Deselects all the currently selected items");
364         GridDataBuilder.create(linkDeselect).hFill().hGrab();
365         linkDeselect.addSelectionListener(new SelectionAdapter() {
366             @Override
367             public void widgetSelected(SelectionEvent e) {
368                 super.widgetSelected(e);
369                 onDeselectAll();
370             }
371         });
372
373         mButtonDelete = new Button(mGroupOptions, SWT.NONE);
374         mButtonDelete.setText("");  //$NON-NLS-1$  placeholder, filled in updateButtonsState()
375         mButtonDelete.setToolTipText("Delete one ore more installed packages");
376         GridDataBuilder.create(mButtonDelete).hFill().vCenter().hGrab();
377         mButtonDelete.addSelectionListener(new SelectionAdapter() {
378             @Override
379             public void widgetSelected(SelectionEvent e) {
380                 onButtonDelete();  //$hide$
381             }
382         });
383     }
384
385     private Image getImage(String filename) {
386         if (mUpdaterData != null) {
387             ImageFactory imgFactory = mUpdaterData.getImageFactory();
388             if (imgFactory != null) {
389                 return imgFactory.getImageByName(filename);
390             }
391         }
392         return null;
393     }
394
395
396     // -- Start of internal part ----------
397     // Hide everything down-below from SWT designer
398     //$hide>>$
399
400
401     // --- menu interactions ---
402
403     public void registerMenuAction(final MenuAction action, MenuItem item) {
404         item.addSelectionListener(new SelectionAdapter() {
405             @Override
406             public void widgetSelected(SelectionEvent e) {
407                 Button button = null;
408
409                 switch (action) {
410                 case RELOAD:
411                     fullReload();
412                     break;
413                 case SHOW_ADDON_SITES:
414                     AddonSitesDialog d = new AddonSitesDialog(getShell(), mUpdaterData);
415                     if (d.open()) {
416                         loadPackages();
417                     }
418                     break;
419                 case TOGGLE_SHOW_ARCHIVES:
420                     mDisplayArchives = !mDisplayArchives;
421                     // Force the viewer to be refreshed
422                     mTreeViewer.setInput(null);
423                     refreshViewerInput();
424                     syncViewerSelection();
425                     updateButtonsState();
426                     break;
427                 case TOGGLE_SHOW_INSTALLED_PKG:
428                     button = mCheckFilterInstalled;
429                     break;
430                 case TOGGLE_SHOW_OBSOLETE_PKG:
431                     button = mCheckFilterObsolete;
432                     break;
433                 case TOGGLE_SHOW_UPDATE_NEW_PKG:
434                     button = mCheckFilterNew;
435                     break;
436                 case SORT_API_LEVEL:
437                     button = mCheckSortApi;
438                     break;
439                 case SORT_SOURCE:
440                     button = mCheckSortSource;
441                     break;
442                 }
443
444                 if (button != null && !button.isDisposed()) {
445                     // Toggle this button (radio or checkbox)
446
447                     boolean value = button.getSelection();
448
449                     // SWT doesn't automatically switch radio buttons when using the
450                     // Widget#setSelection method, so we'll do it here manually.
451                     if (!value && (button.getStyle() & SWT.RADIO) != 0) {
452                         // we'll be selecting this radio button, so deselect all ther other ones
453                         // in the parent group.
454                         for (Control child : button.getParent().getChildren()) {
455                             if (child instanceof Button &&
456                                     child != button &&
457                                     (child.getStyle() & SWT.RADIO) != 0) {
458                                 ((Button) child).setSelection(value);
459                             }
460                         }
461                     }
462
463                     button.setSelection(!value);
464
465                     // SWT doesn't actually invoke the listeners when using Widget#setSelection
466                     // so let's run the actual action.
467                     button.notifyListeners(SWT.Selection, new Event());
468                 }
469
470                 updateMenuCheckmarks();
471             }
472         });
473
474         mMenuActions.put(action, item);
475     }
476
477     // --- internal methods ---
478
479     private void updateMenuCheckmarks() {
480
481         for (Entry<MenuAction, MenuItem> entry : mMenuActions.entrySet()) {
482             MenuAction action = entry.getKey();
483             MenuItem item = entry.getValue();
484
485             if (action.getMenuStyle() == SWT.NONE) {
486                 continue;
487             }
488
489             boolean value = false;
490             Button button = null;
491
492             switch (action) {
493             case TOGGLE_SHOW_ARCHIVES:
494                 value = mDisplayArchives;
495                 break;
496             case TOGGLE_SHOW_INSTALLED_PKG:
497                 button = mCheckFilterInstalled;
498                 break;
499             case TOGGLE_SHOW_OBSOLETE_PKG:
500                 button = mCheckFilterObsolete;
501                 break;
502             case TOGGLE_SHOW_UPDATE_NEW_PKG:
503                 button = mCheckFilterNew;
504                 break;
505             case SORT_API_LEVEL:
506                 button = mCheckSortApi;
507                 break;
508             case SORT_SOURCE:
509                 button = mCheckSortSource;
510                 break;
511             }
512
513             if (button != null && !button.isDisposed()) {
514                 value = button.getSelection();
515             }
516
517             item.setSelection(value);
518         }
519
520     }
521
522     private void postCreate() {
523         if (mUpdaterData != null) {
524             mTextSdkOsPath.setText(mUpdaterData.getOsSdkRoot());
525         }
526
527         mTreeViewer.setContentProvider(new PkgContentProvider());
528         ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE);
529
530         mColumnApi.setLabelProvider     (new PkgTreeColumnViewerLabelProvider(mColumnApi));
531         mColumnName.setLabelProvider    (new PkgTreeColumnViewerLabelProvider(mColumnName));
532         mColumnStatus.setLabelProvider  (new PkgTreeColumnViewerLabelProvider(mColumnStatus));
533         mColumnRevision.setLabelProvider(new PkgTreeColumnViewerLabelProvider(mColumnRevision));
534
535         FontData fontData = mTree.getFont().getFontData()[0];
536         fontData.setStyle(SWT.ITALIC);
537         mTreeFontItalic = new Font(mTree.getDisplay(), fontData);
538
539         mTree.addDisposeListener(new DisposeListener() {
540             public void widgetDisposed(DisposeEvent e) {
541                 mTreeFontItalic.dispose();
542                 mTreeFontItalic = null;
543             }
544         });
545     }
546
547     /**
548      * Performs a full reload by removing all cached packages data, including the platforms
549      * and addons from the sdkmanager instance. This will perform a full local parsing
550      * as well as a full reload of the remote data (by fetching all sources again.)
551      */
552     private void fullReload() {
553         // Clear all source information, forcing them to be refreshed.
554         mUpdaterData.getSources().clearAllPackages();
555         // Clear and reload all local data too.
556         localReload();
557     }
558
559     /**
560      * Performs a full reload of all the local package information, including the platforms
561      * and addons from the sdkmanager instance. This will perform a full local parsing.
562      * <p/>
563      * This method does NOT force a new fetch of the remote sources.
564      *
565      * @see #fullReload()
566      */
567     private void localReload() {
568         // Clear all source caches, otherwise loading will use the cached data
569         mUpdaterData.getLocalSdkParser().clearPackages();
570         mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog());
571         loadPackages();
572     }
573
574     private void loadPackages() {
575         if (mUpdaterData == null) {
576             return;
577         }
578
579         // LoadPackage is synchronous but does not block the UI.
580         // Consequently it's entirely possible for the user
581         // to request the app to close whilst the packages are loading. Any
582         // action done after loadPackages must check the UI hasn't been
583         // disposed yet. Otherwise hilarity ensues.
584
585         final boolean displaySortByApi = isSortByApi();
586
587         if (!mTreeColumnName.isDisposed()) {
588             mTreeColumnName.setImage(
589                     getImage(displaySortByApi ? ICON_SORT_BY_API : ICON_SORT_BY_SOURCE));
590         }
591
592         mDiffLogic.updateStart();
593         mDiffLogic.getPackageLoader().loadPackages(new ISourceLoadedCallback() {
594             public boolean onUpdateSource(SdkSource source, Package[] newPackages) {
595                 // This runs in a thread and must not access UI directly.
596                 final boolean changed = mDiffLogic.updateSourcePackages(
597                         displaySortByApi, source, newPackages);
598
599                 if (!mGroupPackages.isDisposed()) {
600                     mGroupPackages.getDisplay().syncExec(new Runnable() {
601                         public void run() {
602                             if (changed ||
603                                 mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) {
604                                 refreshViewerInput();
605                             }
606                         }
607                     });
608                 }
609
610                 // Return true to tell the loader to continue with the next source.
611                 // Return false to stop the loader if any UI has been disposed, which can
612                 // happen if the user is trying to close the window during the load operation.
613                 return !mGroupPackages.isDisposed();
614             }
615
616             public void onLoadCompleted() {
617                 // This runs in a thread and must not access UI directly.
618                 final boolean changed = mDiffLogic.updateEnd(displaySortByApi);
619
620                 if (!mGroupPackages.isDisposed()) {
621                     mGroupPackages.getDisplay().syncExec(new Runnable() {
622                         public void run() {
623                             if (changed ||
624                                 mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) {
625                                 refreshViewerInput();
626                             }
627
628                             if (mDiffLogic.isFirstLoadComplete() && !mGroupPackages.isDisposed()) {
629                                 // At the end of the first load, if nothing is selected then
630                                 // automatically select all new and update packages.
631                                 Object[] checked = mTreeViewer.getCheckedElements();
632                                 if (checked == null || checked.length == 0) {
633                                     onSelectNewUpdates(
634                                             false, //selectNew
635                                             true,  //selectUpdates,
636                                             true); //selectTop
637                                 }
638                             }
639                         }
640                     });
641                 }
642             }
643         });
644     }
645
646     private void refreshViewerInput() {
647         // Dynamically update the table while we load after each source.
648         // Since the official Android source gets loaded first, it makes the
649         // window look non-empty a lot sooner.
650         if (!mGroupPackages.isDisposed()) {
651
652             List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
653             if (mTreeViewer.getInput() != cats) {
654                 // set initial input
655                 mTreeViewer.setInput(cats);
656             } else {
657                 // refresh existing, which preserves the expanded state, the selection
658                 // and the checked state.
659                 mTreeViewer.refresh();
660             }
661
662             // set the initial expanded state
663             expandInitial(mTreeViewer.getInput());
664
665             updateButtonsState();
666             updateMenuCheckmarks();
667         }
668     }
669
670     private boolean isSortByApi() {
671         return mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection();
672     }
673
674     /**
675      * Decide whether to keep an item in the current tree based on user-chosen filter options.
676      */
677     private boolean filterViewerItem(Object treeElement) {
678         if (treeElement instanceof PkgCategory) {
679             PkgCategory cat = (PkgCategory) treeElement;
680
681             if (!cat.getItems().isEmpty()) {
682                 // A category is hidden if all of its content is hidden.
683                 // However empty categories are always visible.
684                 for (PkgItem item : cat.getItems()) {
685                     if (filterViewerItem(item)) {
686                         // We found at least one element that is visible.
687                         return true;
688                     }
689                 }
690                 return false;
691             }
692         }
693
694         if (treeElement instanceof PkgItem) {
695             PkgItem item = (PkgItem) treeElement;
696
697             if (!mCheckFilterObsolete.getSelection()) {
698                 if (item.isObsolete()) {
699                     return false;
700                 }
701             }
702
703             if (!mCheckFilterInstalled.getSelection()) {
704                 if (item.getState() == PkgState.INSTALLED) {
705                     return false;
706                 }
707             }
708
709             if (!mCheckFilterNew.getSelection()) {
710                 if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) {
711                     return false;
712                 }
713             }
714         }
715
716         return true;
717     }
718
719     /**
720      * Performs the initial expansion of the tree. This expands categories that contain
721      * at least one installed item and collapses the ones with nothing installed.
722      *
723      * TODO: change this to only change the expanded state on categories that have not
724      * been touched by the user yet. Once we do that, call this every time a new source
725      * is added or the list is reloaded.
726      */
727     private void expandInitial(Object elem) {
728         if (elem == null) {
729             return;
730         }
731         if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) {
732             mTreeViewer.setExpandedState(elem, true);
733             for (Object pkg :
734                     ((ITreeContentProvider) mTreeViewer.getContentProvider()).getChildren(elem)) {
735                 if (pkg instanceof PkgCategory) {
736                     PkgCategory cat = (PkgCategory) pkg;
737                     for (PkgItem item : cat.getItems()) {
738                         if (item.getState() == PkgState.INSTALLED) {
739                             expandInitial(pkg);
740                             break;
741                         }
742                     }
743                 }
744             }
745         }
746     }
747
748     /**
749      * Handle checking and unchecking of the tree items.
750      *
751      * When unchecking, all sub-tree items checkboxes are cleared too.
752      * When checking a source, all of its packages are checked too.
753      * When checking a package, only its compatible archives are checked.
754      */
755     private void onTreeCheckStateChanged(CheckStateChangedEvent event) {
756         boolean checked = event.getChecked();
757         Object elem = event.getElement();
758
759         assert event.getSource() == mTreeViewer;
760
761         // When selecting, we want to only select compatible archives and expand the super nodes.
762         checkAndExpandItem(elem, checked, true/*fixChildren*/, true/*fixParent*/);
763         updateButtonsState();
764     }
765
766     private void onTreeDoubleClick(DoubleClickEvent event) {
767         assert event.getSource() == mTreeViewer;
768         ISelection sel = event.getSelection();
769         if (sel.isEmpty() || !(sel instanceof ITreeSelection)) {
770             return;
771         }
772         ITreeSelection tsel = (ITreeSelection) sel;
773         Object elem = tsel.getFirstElement();
774         if (elem == null) {
775             return;
776         }
777
778         ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider();
779         Object[] children = provider.getElements(elem);
780         if (children == null) {
781             return;
782         }
783
784         if (children.length > 0) {
785             // If the element has children, expand/collapse it.
786             if (mTreeViewer.getExpandedState(elem)) {
787                 mTreeViewer.collapseToLevel(elem, 1);
788             } else {
789                 mTreeViewer.expandToLevel(elem, 1);
790             }
791         } else {
792             // If the element is a terminal one, select/deselect it.
793             checkAndExpandItem(
794                     elem,
795                     !mTreeViewer.getChecked(elem),
796                     false /*fixChildren*/,
797                     true /*fixParent*/);
798             updateButtonsState();
799         }
800     }
801
802     private void checkAndExpandItem(
803             Object elem,
804             boolean checked,
805             boolean fixChildren,
806             boolean fixParent) {
807         ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider();
808
809         // fix the item itself
810         if (checked != mTreeViewer.getChecked(elem)) {
811             mTreeViewer.setChecked(elem, checked);
812         }
813         if (elem instanceof PkgItem) {
814             // update the PkgItem to reflect the selection
815             ((PkgItem) elem).setChecked(checked);
816         }
817
818         if (!checked) {
819             if (fixChildren) {
820                 // when de-selecting, we deselect all children too
821                 mTreeViewer.setSubtreeChecked(elem, checked);
822                 for (Object child : provider.getChildren(elem)) {
823                     checkAndExpandItem(child, checked, fixChildren, false/*fixParent*/);
824                 }
825             }
826
827             // fix the parent when deselecting
828             if (fixParent) {
829                 Object parent = provider.getParent(elem);
830                 if (parent != null && mTreeViewer.getChecked(parent)) {
831                     mTreeViewer.setChecked(parent, false);
832                 }
833             }
834             return;
835         }
836
837         // When selecting, we also select sub-items (for a category)
838         if (fixChildren) {
839             if (elem instanceof PkgCategory || elem instanceof PkgItem) {
840                 Object[] children = provider.getChildren(elem);
841                 for (Object child : children) {
842                     checkAndExpandItem(child, true, fixChildren, false/*fixParent*/);
843                 }
844                 // only fix the parent once the last sub-item is set
845                 if (elem instanceof PkgCategory) {
846                     if (children.length > 0) {
847                         checkAndExpandItem(
848                                 children[0], true, false/*fixChildren*/, true/*fixParent*/);
849                     } else {
850                         mTreeViewer.setChecked(elem, false);
851                     }
852                 }
853             } else if (elem instanceof Package) {
854                 // in details mode, we auto-select compatible packages
855                 selectCompatibleArchives(elem, provider);
856             }
857         }
858
859         if (fixParent && checked && elem instanceof PkgItem) {
860             Object parent = provider.getParent(elem);
861             if (!mTreeViewer.getChecked(parent)) {
862                 Object[] children = provider.getChildren(parent);
863                 boolean allChecked = children.length > 0;
864                 for (Object e : children) {
865                     if (!mTreeViewer.getChecked(e)) {
866                         allChecked = false;
867                         break;
868                     }
869                 }
870                 if (allChecked) {
871                     mTreeViewer.setChecked(parent, true);
872                 }
873             }
874         }
875     }
876
877     private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) {
878         for (Object archive : provider.getChildren(pkg)) {
879             if (archive instanceof Archive) {
880                 mTreeViewer.setChecked(archive, ((Archive) archive).isCompatible());
881             }
882         }
883     }
884
885     /**
886      * Checks all PkgItems that are either new or have updates or select top platform
887      * for initial run.
888      */
889     private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) {
890         // This does not update the tree itself, syncViewerSelection does it below.
891         mDiffLogic.checkNewUpdateItems(
892                 selectNew,
893                 selectUpdates,
894                 selectTop,
895                 SdkConstants.CURRENT_PLATFORM);
896         syncViewerSelection();
897         updateButtonsState();
898     }
899
900     /**
901      * Deselect all checked PkgItems.
902      */
903     private void onDeselectAll() {
904         // This does not update the tree itself, syncViewerSelection does it below.
905         mDiffLogic.uncheckAllItems();
906         syncViewerSelection();
907         updateButtonsState();
908     }
909
910     /**
911      * When switching between the tree-by-api and the tree-by-source, copy the selection
912      * (aka the checked items) from one list to the other.
913      * This does not update the tree itself.
914      */
915     private void copySelection(boolean fromSourceToApi) {
916         List<PkgItem> fromItems = mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi);
917         List<PkgItem> toItems = mDiffLogic.getAllPkgItems(fromSourceToApi, !fromSourceToApi);
918
919         // deselect all targets
920         for (PkgItem item : toItems) {
921             item.setChecked(false);
922         }
923
924         // mark new one from the source
925         for (PkgItem source : fromItems) {
926             if (source.isChecked()) {
927                 // There should typically be a corresponding item in the target side
928                 for (PkgItem target : toItems) {
929                     if (target.isSameMainPackageAs(source.getMainPackage())) {
930                         target.setChecked(true);
931                         break;
932                     }
933                 }
934             }
935         }
936     }
937
938     /**
939      * Synchronize the 'checked' state of PkgItems in the tree with their internal isChecked state.
940      */
941     private void syncViewerSelection() {
942         ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider();
943
944         Object input = mTreeViewer.getInput();
945         if (input == null) {
946             return;
947         }
948         for (Object cat : provider.getElements(input)) {
949             Object[] children = provider.getElements(cat);
950             boolean allChecked = children.length > 0;
951             for (Object child : children) {
952                 if (child instanceof PkgItem) {
953                     PkgItem item = (PkgItem) child;
954                     boolean checked = item.isChecked();
955                     allChecked &= checked;
956
957                     if (checked != mTreeViewer.getChecked(item)) {
958                         if (checked) {
959                             if (!mTreeViewer.getExpandedState(cat)) {
960                                 mTreeViewer.setExpandedState(cat, true);
961                             }
962                         }
963                         checkAndExpandItem(item, checked, true/*fixChildren*/, false/*fixParent*/);
964                     }
965                 }
966             }
967
968             if (allChecked != mTreeViewer.getChecked(cat)) {
969                 mTreeViewer.setChecked(cat, allChecked);
970             }
971         }
972     }
973
974     /**
975      * Indicate an install/delete operation is pending.
976      * This disables the install/delete buttons.
977      * Use {@link #endOperationPending()} to revert, typically in a {@code try..finally} block.
978      */
979     private void beginOperationPending() {
980         mOperationPending = true;
981         updateButtonsState();
982     }
983
984     private void endOperationPending() {
985         mOperationPending = false;
986         updateButtonsState();
987     }
988
989     /**
990      * Updates the Install and Delete Package buttons.
991      */
992     private void updateButtonsState() {
993         int numPackages = getArchivesForInstall(null /*archives*/);
994
995         mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending);
996         mButtonInstall.setText(
997                 numPackages == 0 ? "Install packages..." :          // disabled button case
998                     numPackages == 1 ? "Install 1 package..." :
999                         String.format("Install %d packages...", numPackages));
1000
1001         // We can only delete local archives
1002         numPackages = getArchivesToDelete(null /*outMsg*/, null /*outArchives*/);
1003
1004         mButtonDelete.setEnabled((numPackages > 0) && !mOperationPending);
1005         mButtonDelete.setText(
1006                 numPackages == 0 ? "Delete packages..." :           // disabled button case
1007                     numPackages == 1 ? "Delete 1 package..." :
1008                         String.format("Delete %d packages...", numPackages));
1009     }
1010
1011     /**
1012      * Called when the Install Package button is selected.
1013      * Collects the packages to be installed and shows the installation window.
1014      */
1015     private void onButtonInstall() {
1016         ArrayList<Archive> archives = new ArrayList<Archive>();
1017         getArchivesForInstall(archives);
1018
1019         if (mUpdaterData != null) {
1020             boolean needsRefresh = false;
1021             try {
1022                 beginOperationPending();
1023
1024                 List<Archive> installed = mUpdaterData.updateOrInstallAll_WithGUI(
1025                     archives,
1026                     mCheckFilterObsolete.getSelection() /* includeObsoletes */,
1027                     mContext == SdkInvocationContext.IDE ?
1028                             UpdaterData.TOOLS_MSG_UPDATED_FROM_ADT :
1029                                 UpdaterData.TOOLS_MSG_UPDATED_FROM_SDKMAN);
1030                 needsRefresh = installed != null && !installed.isEmpty();
1031             } finally {
1032                 endOperationPending();
1033
1034                 if (needsRefresh) {
1035                     // The local package list has changed, make sure to refresh it
1036                     localReload();
1037                 }
1038             }
1039         }
1040     }
1041
1042     /**
1043      * Selects the archives that can be installed.
1044      * This can be used with a null {@code outArchives} just to count the number of
1045      * installable archives.
1046      *
1047      * @param outArchives An archive list where to add the archives that can be installed.
1048      *   This can be null.
1049      * @return The number of archives that can be installed.
1050      */
1051     private int getArchivesForInstall(List<Archive> outArchives) {
1052         if (mTreeViewer == null ||
1053                 mTreeViewer.getTree() == null ||
1054                 mTreeViewer.getTree().isDisposed()) {
1055             return 0;
1056         }
1057         Object[] checked = mTreeViewer.getCheckedElements();
1058         if (checked == null) {
1059             return 0;
1060         }
1061
1062         int count = 0;
1063
1064         if (mDisplayArchives) {
1065             // In detail mode, we display archives so we can install only the
1066             // archives that are actually selected.
1067             // Note that in this mode we allow the user to install an archive
1068             // even if it's not "compatible" with the current platform or is
1069             // already installed.
1070
1071             for (Object c : checked) {
1072                 if (c instanceof Archive) {
1073                     Archive a = (Archive) c;
1074                     if (a != null) {
1075                         count++;
1076                         if (outArchives != null) {
1077                             outArchives.add((Archive) c);
1078                         }
1079                     }
1080                 }
1081             }
1082         } else {
1083             // In non-detail mode, we install all the compatible archives
1084             // found in the selected pkg items. We also automatically
1085             // select update packages rather than the root package if any.
1086
1087             for (Object c : checked) {
1088                 Package p = null;
1089                 if (c instanceof Package) {
1090                     // This is an update package
1091                     p = (Package) c;
1092                 } else if (c instanceof PkgItem) {
1093                     p = ((PkgItem) c).getMainPackage();
1094
1095                     PkgItem pi = (PkgItem) c;
1096                     if (pi.getState() == PkgState.INSTALLED) {
1097                         // We don't allow installing items that are already installed
1098                         // unless they have a pending update.
1099                         p = pi.getUpdatePkg();
1100
1101                     } else if (pi.getState() == PkgState.NEW) {
1102                         p = pi.getMainPackage();
1103                     }
1104                 }
1105                 if (p != null) {
1106                     for (Archive a : p.getArchives()) {
1107                         if (a != null && a.isCompatible()) {
1108                             count++;
1109                             if (outArchives != null) {
1110                                 outArchives.add(a);
1111                             }
1112                         }
1113                     }
1114                 }
1115             }
1116         }
1117
1118         return count;
1119     }
1120
1121     /**
1122      * Called when the Delete Package button is selected.
1123      * Collects the packages to be deleted, prompt the user for confirmation
1124      * and actually performs the deletion.
1125      */
1126     private void onButtonDelete() {
1127         final String title = "Delete SDK Package";
1128         StringBuilder msg = new StringBuilder("Are you sure you want to delete:");
1129
1130         // A list of archives to delete
1131         final ArrayList<Archive> archives = new ArrayList<Archive>();
1132
1133         getArchivesToDelete(msg, archives);
1134
1135         if (!archives.isEmpty()) {
1136             msg.append("\n").append("This cannot be undone.");  //$NON-NLS-1$
1137             if (MessageDialog.openQuestion(getShell(), title, msg.toString())) {
1138                 try {
1139                     beginOperationPending();
1140
1141                     mUpdaterData.getTaskFactory().start("Delete Package", new ITask() {
1142                         public void run(ITaskMonitor monitor) {
1143                             monitor.setProgressMax(archives.size() + 1);
1144                             for (Archive a : archives) {
1145                                 monitor.setDescription("Deleting '%1$s' (%2$s)",
1146                                         a.getParentPackage().getShortDescription(),
1147                                         a.getLocalOsPath());
1148
1149                                 // Delete the actual package
1150                                 a.deleteLocal();
1151
1152                                 monitor.incProgress(1);
1153                                 if (monitor.isCancelRequested()) {
1154                                     break;
1155                                 }
1156                             }
1157
1158                             monitor.incProgress(1);
1159                             monitor.setDescription("Done");
1160                         }
1161                     });
1162                 } finally {
1163                     endOperationPending();
1164
1165                     // The local package list has changed, make sure to refresh it
1166                     localReload();
1167                 }
1168             }
1169         }
1170     }
1171
1172     /**
1173      * Selects the archives that can be deleted and collect their names.
1174      * This can be used with a null {@code outArchives} and a null {@code outMsg}
1175      * just to count the number of archives to be deleted.
1176      *
1177      * @param outMsg A StringBuilder where the names of the packages to be deleted is
1178      *   accumulated. This is used to confirm deletion with the user.
1179      * @param outArchives An archive list where to add the archives that can be installed.
1180      *   This can be null.
1181      * @return The number of archives that can be deleted.
1182      */
1183     private int getArchivesToDelete(StringBuilder outMsg, List<Archive> outArchives) {
1184         if (mTreeViewer == null ||
1185                 mTreeViewer.getTree() == null ||
1186                 mTreeViewer.getTree().isDisposed()) {
1187             return 0;
1188         }
1189         Object[] checked = mTreeViewer.getCheckedElements();
1190         if (checked == null) {
1191             // This should not happen since the button should be disabled
1192             return 0;
1193         }
1194
1195         int count = 0;
1196
1197         if (mDisplayArchives) {
1198             // In detail mode, select archives that can be deleted
1199
1200             for (Object c : checked) {
1201                 if (c instanceof Archive) {
1202                     Archive a = (Archive) c;
1203                     if (a != null && a.isLocal()) {
1204                         count++;
1205                         if (outMsg != null) {
1206                             String osPath = a.getLocalOsPath();
1207                             File dir = new File(osPath);
1208                             Package p = a.getParentPackage();
1209                             if (p != null && dir.isDirectory()) {
1210                                 outMsg.append("\n - ")    //$NON-NLS-1$
1211                                       .append(p.getShortDescription());
1212                             }
1213                         }
1214                         if (outArchives != null) {
1215                             outArchives.add(a);
1216                         }
1217                     }
1218                 }
1219             }
1220         } else {
1221             // In non-detail mode, select archives of selected packages that can be deleted.
1222
1223             for (Object c : checked) {
1224                 if (c instanceof PkgItem) {
1225                     PkgItem pi = (PkgItem) c;
1226                     PkgState state = pi.getState();
1227                     if (state == PkgState.INSTALLED) {
1228                         Package p = pi.getMainPackage();
1229
1230                         for (Archive a : p.getArchives()) {
1231                             if (a != null && a.isLocal()) {
1232                                 count++;
1233                                 if (outMsg != null) {
1234                                     String osPath = a.getLocalOsPath();
1235                                     File dir = new File(osPath);
1236                                     if (dir.isDirectory()) {
1237                                         outMsg.append("\n - ")    //$NON-NLS-1$
1238                                               .append(p.getShortDescription());
1239                                     }
1240                                 }
1241                                 if (outArchives != null) {
1242                                     outArchives.add(a);
1243                                 }
1244                             }
1245                         }
1246                     }
1247                 }
1248             }
1249         }
1250
1251         return count;
1252     }
1253
1254     // ----------------------
1255
1256     /**
1257      * A custom version of {@link TreeColumnViewerLabelProvider} which
1258      * handles {@link TreePath}s and delegates content to a base
1259      * {@link PkgCellLabelProvider} for the given {@link TreeViewerColumn}.
1260      * <p/>
1261      * The implementation handles a variety of providers (table label, table
1262      * color, table font) but does not implement a tooltip provider, so we
1263      * delegate the calls here to the appropriate {@link PkgCellLabelProvider}.
1264      * <p/>
1265      * Only {@link #getToolTipText(Object)} is really useful for us but we
1266      * delegate all the tooltip calls for completeness and avoid surprises later
1267      * if we ever decide to override more things in the label provider.
1268      */
1269     public class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider {
1270
1271         private CellLabelProvider mTooltipProvider;
1272
1273         public PkgTreeColumnViewerLabelProvider(TreeViewerColumn column) {
1274             super(new PkgCellLabelProvider(column));
1275         }
1276
1277         @Override
1278         public void setProviders(Object provider) {
1279             super.setProviders(provider);
1280             if (provider instanceof CellLabelProvider) {
1281                 mTooltipProvider = (CellLabelProvider) provider;
1282             }
1283         }
1284
1285         @Override
1286         public Image getToolTipImage(Object object) {
1287             if (mTooltipProvider != null) {
1288                 return mTooltipProvider.getToolTipImage(object);
1289             }
1290             return super.getToolTipImage(object);
1291         }
1292
1293         @Override
1294         public String getToolTipText(Object element) {
1295             if (mTooltipProvider != null) {
1296                 return mTooltipProvider.getToolTipText(element);
1297             }
1298             return super.getToolTipText(element);
1299         }
1300
1301         @Override
1302         public Color getToolTipBackgroundColor(Object object) {
1303             if (mTooltipProvider != null) {
1304                 return mTooltipProvider.getToolTipBackgroundColor(object);
1305             }
1306             return super.getToolTipBackgroundColor(object);
1307         }
1308
1309         @Override
1310         public Color getToolTipForegroundColor(Object object) {
1311             if (mTooltipProvider != null) {
1312                 return mTooltipProvider.getToolTipForegroundColor(object);
1313             }
1314             return super.getToolTipForegroundColor(object);
1315         }
1316
1317         @Override
1318         public Font getToolTipFont(Object object) {
1319             if (mTooltipProvider != null) {
1320                 return mTooltipProvider.getToolTipFont(object);
1321             }
1322             return super.getToolTipFont(object);
1323         }
1324
1325         @Override
1326         public Point getToolTipShift(Object object) {
1327             if (mTooltipProvider != null) {
1328                 return mTooltipProvider.getToolTipShift(object);
1329             }
1330             return super.getToolTipShift(object);
1331         }
1332
1333         @Override
1334         public boolean useNativeToolTip(Object object) {
1335             if (mTooltipProvider != null) {
1336                 return mTooltipProvider.useNativeToolTip(object);
1337             }
1338             return super.useNativeToolTip(object);
1339         }
1340
1341         @Override
1342         public int getToolTipTimeDisplayed(Object object) {
1343             if (mTooltipProvider != null) {
1344                 return mTooltipProvider.getToolTipTimeDisplayed(object);
1345             }
1346             return super.getToolTipTimeDisplayed(object);
1347         }
1348
1349         @Override
1350         public int getToolTipDisplayDelayTime(Object object) {
1351             if (mTooltipProvider != null) {
1352                 return mTooltipProvider.getToolTipDisplayDelayTime(object);
1353             }
1354             return super.getToolTipDisplayDelayTime(object);
1355         }
1356
1357         @Override
1358         public int getToolTipStyle(Object object) {
1359             if (mTooltipProvider != null) {
1360                 return mTooltipProvider.getToolTipStyle(object);
1361             }
1362             return super.getToolTipStyle(object);
1363         }
1364     }
1365
1366     public class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider {
1367
1368         private final TreeViewerColumn mColumn;
1369
1370         public PkgCellLabelProvider(TreeViewerColumn column) {
1371             super();
1372             mColumn = column;
1373         }
1374
1375         @Override
1376         public String getText(Object element) {
1377
1378             if (mColumn == mColumnName) {
1379
1380                 if (element instanceof PkgCategory) {
1381                     return ((PkgCategory) element).getLabel();
1382                 } else if (element instanceof PkgItem) {
1383                     return getPkgItemName((PkgItem) element);
1384                 } else if (element instanceof IDescription) {
1385                     return ((IDescription) element).getShortDescription();
1386                 }
1387
1388             } else if (mColumn == mColumnApi) {
1389
1390                 int api = -1;
1391                 if (element instanceof PkgItem) {
1392                     api = ((PkgItem) element).getApi();
1393                 }
1394                 if (api >= 1) {
1395                     return Integer.toString(api);
1396                 }
1397
1398             } else if (mColumn == mColumnRevision) {
1399
1400                 if (element instanceof PkgItem) {
1401                     PkgItem pkg = (PkgItem) element;
1402                     return Integer.toString(pkg.getRevision());
1403                 }
1404
1405             } else if (mColumn == mColumnStatus) {
1406
1407                 if (element instanceof PkgItem) {
1408                     PkgItem pkg = (PkgItem) element;
1409
1410                     switch(pkg.getState()) {
1411                     case INSTALLED:
1412                         Package update = pkg.getUpdatePkg();
1413                         if (update != null) {
1414                             return String.format(
1415                                     "Update available: rev. %1$s",
1416                                     update.getRevision());
1417                         }
1418                         return "Installed";
1419
1420                     case NEW:
1421                         return "Not installed";
1422                     }
1423                     return pkg.getState().toString();
1424
1425                 } else if (element instanceof Package) {
1426                     // This is an update package.
1427                     return "New revision " + Integer.toString(((Package) element).getRevision());
1428                 }
1429             }
1430
1431             return "";
1432         }
1433
1434         private String getPkgItemName(PkgItem item) {
1435             String name = item.getName().trim();
1436
1437             if (isSortByApi()) {
1438                 // When sorting by API, the package name might contains the API number
1439                 // or the platform name at the end. If we find it, cut it out since it's
1440                 // redundant.
1441
1442                 PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item);
1443                 String apiLabel = cat.getApiLabel();
1444                 String platLabel = cat.getPlatformName();
1445
1446                 if (platLabel != null && name.endsWith(platLabel)) {
1447                     return name.substring(0, name.length() - platLabel.length());
1448
1449                 } else if (apiLabel != null && name.endsWith(apiLabel)) {
1450                     return name.substring(0, name.length() - apiLabel.length());
1451
1452                 } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) {
1453                     // For obsolete items, the format is "<base name> <platform name> (Obsolete)"
1454                     // so in this case only accept removing a platform name that is not at
1455                     // the end.
1456                     name = name.replace(platLabel, ""); //$NON-NLS-1$
1457                 }
1458             }
1459
1460             // Collapse potential duplicated spacing
1461             name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$
1462
1463             return name;
1464         }
1465
1466         private PkgCategory findCategoryForItem(PkgItem item) {
1467             List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
1468             for (PkgCategory cat : cats) {
1469                 for (PkgItem i : cat.getItems()) {
1470                     if (i == item) {
1471                         return cat;
1472                     }
1473                 }
1474             }
1475
1476             return null;
1477         }
1478
1479         @Override
1480         public Image getImage(Object element) {
1481             ImageFactory imgFactory = mUpdaterData.getImageFactory();
1482
1483             if (imgFactory != null) {
1484                 if (mColumn == mColumnName) {
1485                     if (element instanceof PkgCategory) {
1486                         return imgFactory.getImageForObject(((PkgCategory) element).getIconRef());
1487                     } else if (element instanceof PkgItem) {
1488                         return imgFactory.getImageForObject(((PkgItem) element).getMainPackage());
1489                     }
1490                     return imgFactory.getImageForObject(element);
1491
1492                 } else if (mColumn == mColumnStatus && element instanceof PkgItem) {
1493                     PkgItem pi = (PkgItem) element;
1494                     switch(pi.getState()) {
1495                     case INSTALLED:
1496                         if (pi.hasUpdatePkg()) {
1497                             return imgFactory.getImageByName(ICON_PKG_UPDATE);
1498                         } else {
1499                             return imgFactory.getImageByName(ICON_PKG_INSTALLED);
1500                         }
1501                     case NEW:
1502                         return imgFactory.getImageByName(ICON_PKG_NEW);
1503                     }
1504                 }
1505             }
1506             return super.getImage(element);
1507         }
1508
1509         // -- ITableFontProvider
1510
1511         public Font getFont(Object element, int columnIndex) {
1512             if (element instanceof PkgItem) {
1513                 if (((PkgItem) element).getState() == PkgState.NEW) {
1514                     return mTreeFontItalic;
1515                 }
1516             } else if (element instanceof Package) {
1517                 // update package
1518                 return mTreeFontItalic;
1519             }
1520             return super.getFont(element);
1521         }
1522
1523         // -- Tooltip support
1524
1525         @Override
1526         public String getToolTipText(Object element) {
1527             if (element instanceof PkgItem) {
1528                 element = ((PkgItem) element).getMainPackage();
1529             }
1530             if (element instanceof IDescription) {
1531                 String s = ((IDescription) element).getLongDescription();
1532                 if (element instanceof Package) {
1533                     SdkSource src = ((Package) element).getParentSource();
1534                     if (src != null) {
1535                         try {
1536                             URL url = new URL(src.getUrl());
1537                             String host = url.getHost();
1538                             if (((Package) element).isLocal()) {
1539                                 s += String.format("\nInstalled from %1$s", host);
1540                             } else {
1541                                 s += String.format("\nProvided by %1$s", host);
1542                             }
1543                         } catch (MalformedURLException ignore) {
1544                         }
1545                     }
1546                 }
1547                 return s;
1548             }
1549             return super.getToolTipText(element);
1550         }
1551
1552         @Override
1553         public Point getToolTipShift(Object object) {
1554             return new Point(15, 5);
1555         }
1556
1557         @Override
1558         public int getToolTipDisplayDelayTime(Object object) {
1559             return 500;
1560         }
1561     }
1562
1563     private class PkgContentProvider implements ITreeContentProvider {
1564
1565         public Object[] getChildren(Object parentElement) {
1566             if (parentElement instanceof ArrayList<?>) {
1567                 return ((ArrayList<?>) parentElement).toArray();
1568
1569             } else if (parentElement instanceof PkgCategory) {
1570                 return ((PkgCategory) parentElement).getItems().toArray();
1571
1572             } else if (parentElement instanceof PkgItem) {
1573                 if (mDisplayArchives) {
1574
1575                     Package pkg = ((PkgItem) parentElement).getUpdatePkg();
1576
1577                     // Display update packages as sub-items if the details mode is activated.
1578                     if (pkg != null) {
1579                         return new Object[] { pkg };
1580                     }
1581
1582                     return ((PkgItem) parentElement).getArchives();
1583                 }
1584
1585             } else if (parentElement instanceof Package) {
1586                 if (mDisplayArchives) {
1587                     return ((Package) parentElement).getArchives();
1588                 }
1589
1590             }
1591
1592             return new Object[0];
1593         }
1594
1595         @SuppressWarnings("unchecked")
1596         public Object getParent(Object element) {
1597             // This operation is expensive, so we do the minimum
1598             // and don't try to cover all cases.
1599
1600             if (element instanceof PkgItem) {
1601                 Object input = mTreeViewer.getInput();
1602                 if (input != null) {
1603                     for (PkgCategory cat : (List<PkgCategory>) input) {
1604                         if (cat.getItems().contains(element)) {
1605                             return cat;
1606                         }
1607                     }
1608                 }
1609             }
1610
1611             return null;
1612         }
1613
1614         public boolean hasChildren(Object parentElement) {
1615             if (parentElement instanceof ArrayList<?>) {
1616                 return true;
1617
1618             } else if (parentElement instanceof PkgCategory) {
1619                 return true;
1620
1621             } else if (parentElement instanceof PkgItem) {
1622                 if (mDisplayArchives) {
1623                     Package pkg = ((PkgItem) parentElement).getUpdatePkg();
1624
1625                     // Display update packages as sub-items if the details mode is activated.
1626                     if (pkg != null) {
1627                         return true;
1628                     }
1629
1630                     Archive[] archives = ((PkgItem) parentElement).getArchives();
1631                     return archives.length > 0;
1632                 }
1633             } else if (parentElement instanceof Package) {
1634                 if (mDisplayArchives) {
1635                     return ((Package) parentElement).getArchives().length > 0;
1636                 }
1637             }
1638
1639             return false;
1640         }
1641
1642         public Object[] getElements(Object inputElement) {
1643             return getChildren(inputElement);
1644         }
1645
1646         public void dispose() {
1647             // unused
1648
1649         }
1650
1651         public void inputChanged(Viewer arg0, Object arg1, Object arg2) {
1652             // unused
1653         }
1654     }
1655
1656     // --- Implementation of ISdkChangeListener ---
1657
1658     public void onSdkLoaded() {
1659         onSdkReload();
1660     }
1661
1662     public void onSdkReload() {
1663         // The sdkmanager finished reloading its data. We must not call localReload() from here
1664         // since we don't want to alter the sdkmanager's data that just finished loading.
1665         loadPackages();
1666     }
1667
1668     public void preInstallHook() {
1669         // nothing to be done for now.
1670     }
1671
1672     public void postInstallHook() {
1673         // nothing to be done for now.
1674     }
1675
1676
1677     // --- End of hiding from SWT Designer ---
1678     //$hide<<$
1679 }