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