Java tutorial
/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdkuilib.internal.repository.sdkman2; import com.android.sdklib.SdkConstants; import com.android.sdklib.internal.repository.Archive; import com.android.sdklib.internal.repository.ArchiveInstaller; import com.android.sdklib.internal.repository.IDescription; import com.android.sdklib.internal.repository.ITask; import com.android.sdklib.internal.repository.ITaskMonitor; import com.android.sdklib.internal.repository.Package; import com.android.sdklib.internal.repository.SdkSource; import com.android.sdkuilib.internal.repository.IPageListener; import com.android.sdkuilib.internal.repository.UpdaterData; import com.android.sdkuilib.internal.repository.UpdaterPage; import com.android.sdkuilib.internal.repository.icons.ImageFactory; import com.android.sdkuilib.internal.repository.sdkman2.PackageLoader.ISourceLoadedCallback; import com.android.sdkuilib.internal.repository.sdkman2.PkgItem.PkgState; import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTreeViewer; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ITableFontProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.jface.window.ToolTip; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * Page that displays both locally installed packages as well as all known * remote available packages. This gives an overview of what is installed * vs what is available and allows the user to update or install packages. */ public class PackagesPage extends UpdaterPage implements ISdkChangeListener, IPageListener { static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$ static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$ static final String ICON_SORT_BY_SOURCE = "source_icon16.png"; //$NON-NLS-1$ static final String ICON_SORT_BY_API = "platform_pkg_16.png"; //$NON-NLS-1$ static final String ICON_PKG_NEW = "pkg_new_16.png"; //$NON-NLS-1$ static final String ICON_PKG_INCOMPAT = "pkg_incompat_16.png"; //$NON-NLS-1$ static final String ICON_PKG_UPDATE = "pkg_update_16.png"; //$NON-NLS-1$ static final String ICON_PKG_INSTALLED = "pkg_installed_16.png"; //$NON-NLS-1$ enum MenuAction { RELOAD(SWT.NONE, "Reload"), SHOW_ADDON_SITES(SWT.NONE, "Manage Add-on Sites..."), TOGGLE_SHOW_ARCHIVES( SWT.CHECK, "Show Archives Details"), TOGGLE_SHOW_INSTALLED_PKG(SWT.CHECK, "Show Installed Packages"), TOGGLE_SHOW_OBSOLETE_PKG(SWT.CHECK, "Show Obsolete Packages"), TOGGLE_SHOW_UPDATE_NEW_PKG(SWT.CHECK, "Show Updates/New Packages"), SORT_API_LEVEL(SWT.RADIO, "Sort by API Level"), SORT_SOURCE(SWT.RADIO, "Sort by Repository"); private final int mMenuStyle; private final String mMenuTitle; MenuAction(int menuStyle, String menuTitle) { mMenuStyle = menuStyle; mMenuTitle = menuTitle; } public int getMenuStyle() { return mMenuStyle; } public String getMenuTitle() { return mMenuTitle; } }; private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>(); private final SdkInvocationContext mContext; private final UpdaterData mUpdaterData; private final PackagesDiffLogic mDiffLogic; private boolean mDisplayArchives = false; private boolean mOperationPending; private Text mTextSdkOsPath; private Button mCheckSortSource; private Button mCheckSortApi; private Button mCheckFilterObsolete; private Button mCheckFilterInstalled; private Button mCheckFilterNew; private Composite mGroupOptions; private Composite mGroupSdk; private Group mGroupPackages; private Button mButtonDelete; private Button mButtonInstall; private Tree mTree; private CheckboxTreeViewer mTreeViewer; private TreeViewerColumn mColumnName; private TreeViewerColumn mColumnApi; private TreeViewerColumn mColumnRevision; private TreeViewerColumn mColumnStatus; private Font mTreeFontItalic; private TreeColumn mTreeColumnName; public PackagesPage(Composite parent, int swtStyle, UpdaterData updaterData, SdkInvocationContext context) { super(parent, swtStyle); mUpdaterData = updaterData; mContext = context; mDiffLogic = new PackagesDiffLogic(updaterData); createContents(this); postCreate(); //$hide$ } public void onPageSelected() { List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); if (cats == null || cats.isEmpty()) { // Initialize the package list the first time the page is shown. loadPackages(); } } @SuppressWarnings("unused") private void createContents(Composite parent) { GridLayoutBuilder.create(parent).noMargins().columns(2); mGroupSdk = new Composite(parent, SWT.NONE); GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2); GridLayoutBuilder.create(mGroupSdk).columns(2); Label label1 = new Label(mGroupSdk, SWT.NONE); label1.setText("SDK Path:"); mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE); GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab(); mTextSdkOsPath.setEnabled(false); mGroupPackages = new Group(parent, SWT.NONE); GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2); mGroupPackages.setText("Packages"); GridLayoutBuilder.create(mGroupPackages).columns(1); mTreeViewer = new CheckboxTreeViewer(mGroupPackages, SWT.BORDER); mTreeViewer.addFilter(new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { return filterViewerItem(element); } }); mTreeViewer.addCheckStateListener(new ICheckStateListener() { public void checkStateChanged(CheckStateChangedEvent event) { onTreeCheckStateChanged(event); //$hide$ } }); mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(DoubleClickEvent event) { onTreeDoubleClick(event); //$hide$ } }); mTree = mTreeViewer.getTree(); mTree.setLinesVisible(true); mTree.setHeaderVisible(true); GridDataBuilder.create(mTree).fill().grab(); // column name icon is set when loading depending on the current filter type // (e.g. API level or source) mColumnName = new TreeViewerColumn(mTreeViewer, SWT.NONE); mTreeColumnName = mColumnName.getColumn(); mTreeColumnName.setText("Name"); mTreeColumnName.setWidth(340); mColumnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE); TreeColumn treeColumn2 = mColumnApi.getColumn(); treeColumn2.setText("API"); treeColumn2.setAlignment(SWT.CENTER); treeColumn2.setWidth(50); mColumnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE); TreeColumn treeColumn3 = mColumnRevision.getColumn(); treeColumn3.setText("Rev."); treeColumn3.setToolTipText("Revision currently installed"); treeColumn3.setAlignment(SWT.CENTER); treeColumn3.setWidth(50); mColumnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE); TreeColumn treeColumn4 = mColumnStatus.getColumn(); treeColumn4.setText("Status"); treeColumn4.setAlignment(SWT.LEAD); treeColumn4.setWidth(190); mGroupOptions = new Composite(mGroupPackages, SWT.NONE); GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab(); GridLayoutBuilder.create(mGroupOptions).columns(6).noMargins(); // Options line 1, 6 columns Label label3 = new Label(mGroupOptions, SWT.NONE); label3.setText("Show:"); mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK); mCheckFilterNew.setText("Updates/New"); mCheckFilterNew.setToolTipText("Show Updates and New"); mCheckFilterNew.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { refreshViewerInput(); } }); mCheckFilterNew.setSelection(true); mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK); mCheckFilterInstalled.setToolTipText("Show Installed"); mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { refreshViewerInput(); } }); mCheckFilterInstalled.setSelection(true); mCheckFilterInstalled.setText("Installed"); mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK); mCheckFilterObsolete.setText("Obsolete"); mCheckFilterObsolete.setToolTipText("Also show obsolete packages"); mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { refreshViewerInput(); } }); mCheckFilterObsolete.setSelection(false); Link linkSelectNew = new Link(mGroupOptions, SWT.NONE); // Note for i18n: we need to identify which link is used, and this is done by using the // text itself so for translation purposes we want to keep the <a> link strings separate. final String strLinkNew = "New"; final String strLinkUpdates = "Updates"; linkSelectNew.setText(String.format("Select <a>%1$s</a> or <a>%2$s</a>", strLinkNew, strLinkUpdates)); linkSelectNew.setToolTipText("Selects all items that are either new or updates."); GridDataBuilder.create(linkSelectNew).hFill().hGrab(); linkSelectNew.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { super.widgetSelected(e); boolean selectNew = e.text == null || e.text.equals(strLinkNew); onSelectNewUpdates(selectNew, !selectNew, false/*selectTop*/); } }); mButtonInstall = new Button(mGroupOptions, SWT.NONE); mButtonInstall.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() mButtonInstall.setToolTipText("Install one or more packages"); GridDataBuilder.create(mButtonInstall).hFill().vCenter().hGrab(); mButtonInstall.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onButtonInstall(); //$hide$ } }); // Options line 2, 6 columns Label label2 = new Label(mGroupOptions, SWT.NONE); label2.setText("Sort by:"); mCheckSortApi = new Button(mGroupOptions, SWT.RADIO); mCheckSortApi.setToolTipText("Sort by API level"); mCheckSortApi.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mCheckSortApi.getSelection()) { refreshViewerInput(); copySelection(true /*toApi*/); syncViewerSelection(); } } }); mCheckSortApi.setText("API level"); mCheckSortApi.setSelection(true); mCheckSortSource = new Button(mGroupOptions, SWT.RADIO); mCheckSortSource.setText("Repository"); mCheckSortSource.setToolTipText("Sort by Repository"); mCheckSortSource.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mCheckSortSource.getSelection()) { refreshViewerInput(); copySelection(false /*toApi*/); syncViewerSelection(); } } }); new Label(mGroupOptions, SWT.NONE); Link linkDeselect = new Link(mGroupOptions, SWT.NONE); linkDeselect.setText("<a>Deselect All</a>"); linkDeselect.setToolTipText("Deselects all the currently selected items"); GridDataBuilder.create(linkDeselect).hFill().hGrab(); linkDeselect.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { super.widgetSelected(e); onDeselectAll(); } }); mButtonDelete = new Button(mGroupOptions, SWT.NONE); mButtonDelete.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() mButtonDelete.setToolTipText("Delete one ore more installed packages"); GridDataBuilder.create(mButtonDelete).hFill().vCenter().hGrab(); mButtonDelete.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onButtonDelete(); //$hide$ } }); } private Image getImage(String filename) { if (mUpdaterData != null) { ImageFactory imgFactory = mUpdaterData.getImageFactory(); if (imgFactory != null) { return imgFactory.getImageByName(filename); } } return null; } // -- Start of internal part ---------- // Hide everything down-below from SWT designer //$hide>>$ // --- menu interactions --- public void registerMenuAction(final MenuAction action, MenuItem item) { item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Button button = null; switch (action) { case RELOAD: fullReload(); break; case SHOW_ADDON_SITES: AddonSitesDialog d = new AddonSitesDialog(getShell(), mUpdaterData); if (d.open()) { loadPackages(); } break; case TOGGLE_SHOW_ARCHIVES: mDisplayArchives = !mDisplayArchives; // Force the viewer to be refreshed mTreeViewer.setInput(null); refreshViewerInput(); syncViewerSelection(); updateButtonsState(); break; case TOGGLE_SHOW_INSTALLED_PKG: button = mCheckFilterInstalled; break; case TOGGLE_SHOW_OBSOLETE_PKG: button = mCheckFilterObsolete; break; case TOGGLE_SHOW_UPDATE_NEW_PKG: button = mCheckFilterNew; break; case SORT_API_LEVEL: button = mCheckSortApi; break; case SORT_SOURCE: button = mCheckSortSource; break; } if (button != null && !button.isDisposed()) { // Toggle this button (radio or checkbox) boolean value = button.getSelection(); // SWT doesn't automatically switch radio buttons when using the // Widget#setSelection method, so we'll do it here manually. if (!value && (button.getStyle() & SWT.RADIO) != 0) { // we'll be selecting this radio button, so deselect all ther other ones // in the parent group. for (Control child : button.getParent().getChildren()) { if (child instanceof Button && child != button && (child.getStyle() & SWT.RADIO) != 0) { ((Button) child).setSelection(value); } } } button.setSelection(!value); // SWT doesn't actually invoke the listeners when using Widget#setSelection // so let's run the actual action. button.notifyListeners(SWT.Selection, new Event()); } updateMenuCheckmarks(); } }); mMenuActions.put(action, item); } // --- internal methods --- private void updateMenuCheckmarks() { for (Entry<MenuAction, MenuItem> entry : mMenuActions.entrySet()) { MenuAction action = entry.getKey(); MenuItem item = entry.getValue(); if (action.getMenuStyle() == SWT.NONE) { continue; } boolean value = false; Button button = null; switch (action) { case TOGGLE_SHOW_ARCHIVES: value = mDisplayArchives; break; case TOGGLE_SHOW_INSTALLED_PKG: button = mCheckFilterInstalled; break; case TOGGLE_SHOW_OBSOLETE_PKG: button = mCheckFilterObsolete; break; case TOGGLE_SHOW_UPDATE_NEW_PKG: button = mCheckFilterNew; break; case SORT_API_LEVEL: button = mCheckSortApi; break; case SORT_SOURCE: button = mCheckSortSource; break; } if (button != null && !button.isDisposed()) { value = button.getSelection(); } item.setSelection(value); } } private void postCreate() { if (mUpdaterData != null) { mTextSdkOsPath.setText(mUpdaterData.getOsSdkRoot()); } mTreeViewer.setContentProvider(new PkgContentProvider()); ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); mColumnApi.setLabelProvider(new PkgTreeColumnViewerLabelProvider(mColumnApi)); mColumnName.setLabelProvider(new PkgTreeColumnViewerLabelProvider(mColumnName)); mColumnStatus.setLabelProvider(new PkgTreeColumnViewerLabelProvider(mColumnStatus)); mColumnRevision.setLabelProvider(new PkgTreeColumnViewerLabelProvider(mColumnRevision)); FontData fontData = mTree.getFont().getFontData()[0]; fontData.setStyle(SWT.ITALIC); mTreeFontItalic = new Font(mTree.getDisplay(), fontData); mTree.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { mTreeFontItalic.dispose(); mTreeFontItalic = null; } }); } /** * Performs a full reload by removing all cached packages data, including the platforms * and addons from the sdkmanager instance. This will perform a full local parsing * as well as a full reload of the remote data (by fetching all sources again.) */ private void fullReload() { // Clear all source information, forcing them to be refreshed. mUpdaterData.getSources().clearAllPackages(); // Clear and reload all local data too. localReload(); } /** * Performs a full reload of all the local package information, including the platforms * and addons from the sdkmanager instance. This will perform a full local parsing. * <p/> * This method does NOT force a new fetch of the remote sources. * * @see #fullReload() */ private void localReload() { // Clear all source caches, otherwise loading will use the cached data mUpdaterData.getLocalSdkParser().clearPackages(); mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); loadPackages(); } private void loadPackages() { if (mUpdaterData == null) { return; } // LoadPackage is synchronous but does not block the UI. // Consequently it's entirely possible for the user // to request the app to close whilst the packages are loading. Any // action done after loadPackages must check the UI hasn't been // disposed yet. Otherwise hilarity ensues. final boolean displaySortByApi = isSortByApi(); if (!mTreeColumnName.isDisposed()) { mTreeColumnName.setImage(getImage(displaySortByApi ? ICON_SORT_BY_API : ICON_SORT_BY_SOURCE)); } mDiffLogic.updateStart(); mDiffLogic.getPackageLoader().loadPackages(new ISourceLoadedCallback() { public boolean onUpdateSource(SdkSource source, Package[] newPackages) { // This runs in a thread and must not access UI directly. final boolean changed = mDiffLogic.updateSourcePackages(displaySortByApi, source, newPackages); if (!mGroupPackages.isDisposed()) { mGroupPackages.getDisplay().syncExec(new Runnable() { public void run() { if (changed || mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { refreshViewerInput(); } } }); } // Return true to tell the loader to continue with the next source. // Return false to stop the loader if any UI has been disposed, which can // happen if the user is trying to close the window during the load operation. return !mGroupPackages.isDisposed(); } public void onLoadCompleted() { // This runs in a thread and must not access UI directly. final boolean changed = mDiffLogic.updateEnd(displaySortByApi); if (!mGroupPackages.isDisposed()) { mGroupPackages.getDisplay().syncExec(new Runnable() { public void run() { if (changed || mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { refreshViewerInput(); } if (mDiffLogic.isFirstLoadComplete() && !mGroupPackages.isDisposed()) { // At the end of the first load, if nothing is selected then // automatically select all new and update packages. Object[] checked = mTreeViewer.getCheckedElements(); if (checked == null || checked.length == 0) { onSelectNewUpdates(false, //selectNew true, //selectUpdates, true); //selectTop } } } }); } } }); } private void refreshViewerInput() { // Dynamically update the table while we load after each source. // Since the official Android source gets loaded first, it makes the // window look non-empty a lot sooner. if (!mGroupPackages.isDisposed()) { List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); if (mTreeViewer.getInput() != cats) { // set initial input mTreeViewer.setInput(cats); } else { // refresh existing, which preserves the expanded state, the selection // and the checked state. mTreeViewer.refresh(); } // set the initial expanded state expandInitial(mTreeViewer.getInput()); updateButtonsState(); updateMenuCheckmarks(); } } private boolean isSortByApi() { return mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection(); } /** * Decide whether to keep an item in the current tree based on user-chosen filter options. */ private boolean filterViewerItem(Object treeElement) { if (treeElement instanceof PkgCategory) { PkgCategory cat = (PkgCategory) treeElement; if (!cat.getItems().isEmpty()) { // A category is hidden if all of its content is hidden. // However empty categories are always visible. for (PkgItem item : cat.getItems()) { if (filterViewerItem(item)) { // We found at least one element that is visible. return true; } } return false; } } if (treeElement instanceof PkgItem) { PkgItem item = (PkgItem) treeElement; if (!mCheckFilterObsolete.getSelection()) { if (item.isObsolete()) { return false; } } if (!mCheckFilterInstalled.getSelection()) { if (item.getState() == PkgState.INSTALLED) { return false; } } if (!mCheckFilterNew.getSelection()) { if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) { return false; } } } return true; } /** * Performs the initial expansion of the tree. This expands categories that contain * at least one installed item and collapses the ones with nothing installed. * * TODO: change this to only change the expanded state on categories that have not * been touched by the user yet. Once we do that, call this every time a new source * is added or the list is reloaded. */ private void expandInitial(Object elem) { if (elem == null) { return; } if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) { mTreeViewer.setExpandedState(elem, true); for (Object pkg : ((ITreeContentProvider) mTreeViewer.getContentProvider()).getChildren(elem)) { if (pkg instanceof PkgCategory) { PkgCategory cat = (PkgCategory) pkg; for (PkgItem item : cat.getItems()) { if (item.getState() == PkgState.INSTALLED) { expandInitial(pkg); break; } } } } } } /** * Handle checking and unchecking of the tree items. * * When unchecking, all sub-tree items checkboxes are cleared too. * When checking a source, all of its packages are checked too. * When checking a package, only its compatible archives are checked. */ private void onTreeCheckStateChanged(CheckStateChangedEvent event) { boolean checked = event.getChecked(); Object elem = event.getElement(); assert event.getSource() == mTreeViewer; // When selecting, we want to only select compatible archives and expand the super nodes. checkAndExpandItem(elem, checked, true/*fixChildren*/, true/*fixParent*/); updateButtonsState(); } private void onTreeDoubleClick(DoubleClickEvent event) { assert event.getSource() == mTreeViewer; ISelection sel = event.getSelection(); if (sel.isEmpty() || !(sel instanceof ITreeSelection)) { return; } ITreeSelection tsel = (ITreeSelection) sel; Object elem = tsel.getFirstElement(); if (elem == null) { return; } ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); Object[] children = provider.getElements(elem); if (children == null) { return; } if (children.length > 0) { // If the element has children, expand/collapse it. if (mTreeViewer.getExpandedState(elem)) { mTreeViewer.collapseToLevel(elem, 1); } else { mTreeViewer.expandToLevel(elem, 1); } } else { // If the element is a terminal one, select/deselect it. checkAndExpandItem(elem, !mTreeViewer.getChecked(elem), false /*fixChildren*/, true /*fixParent*/); updateButtonsState(); } } private void checkAndExpandItem(Object elem, boolean checked, boolean fixChildren, boolean fixParent) { ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); // fix the item itself if (checked != mTreeViewer.getChecked(elem)) { mTreeViewer.setChecked(elem, checked); } if (elem instanceof PkgItem) { // update the PkgItem to reflect the selection ((PkgItem) elem).setChecked(checked); } if (!checked) { if (fixChildren) { // when de-selecting, we deselect all children too mTreeViewer.setSubtreeChecked(elem, checked); for (Object child : provider.getChildren(elem)) { checkAndExpandItem(child, checked, fixChildren, false/*fixParent*/); } } // fix the parent when deselecting if (fixParent) { Object parent = provider.getParent(elem); if (parent != null && mTreeViewer.getChecked(parent)) { mTreeViewer.setChecked(parent, false); } } return; } // When selecting, we also select sub-items (for a category) if (fixChildren) { if (elem instanceof PkgCategory || elem instanceof PkgItem) { Object[] children = provider.getChildren(elem); for (Object child : children) { checkAndExpandItem(child, true, fixChildren, false/*fixParent*/); } // only fix the parent once the last sub-item is set if (elem instanceof PkgCategory) { if (children.length > 0) { checkAndExpandItem(children[0], true, false/*fixChildren*/, true/*fixParent*/); } else { mTreeViewer.setChecked(elem, false); } } } else if (elem instanceof Package) { // in details mode, we auto-select compatible packages selectCompatibleArchives(elem, provider); } } if (fixParent && checked && elem instanceof PkgItem) { Object parent = provider.getParent(elem); if (!mTreeViewer.getChecked(parent)) { Object[] children = provider.getChildren(parent); boolean allChecked = children.length > 0; for (Object e : children) { if (!mTreeViewer.getChecked(e)) { allChecked = false; break; } } if (allChecked) { mTreeViewer.setChecked(parent, true); } } } } private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) { for (Object archive : provider.getChildren(pkg)) { if (archive instanceof Archive) { mTreeViewer.setChecked(archive, ((Archive) archive).isCompatible()); } } } /** * Checks all PkgItems that are either new or have updates or select top platform * for initial run. */ private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { // This does not update the tree itself, syncViewerSelection does it below. mDiffLogic.checkNewUpdateItems(selectNew, selectUpdates, selectTop, SdkConstants.CURRENT_PLATFORM); syncViewerSelection(); updateButtonsState(); } /** * Deselect all checked PkgItems. */ private void onDeselectAll() { // This does not update the tree itself, syncViewerSelection does it below. mDiffLogic.uncheckAllItems(); syncViewerSelection(); updateButtonsState(); } /** * When switching between the tree-by-api and the tree-by-source, copy the selection * (aka the checked items) from one list to the other. * This does not update the tree itself. */ private void copySelection(boolean fromSourceToApi) { List<PkgItem> fromItems = mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi); List<PkgItem> toItems = mDiffLogic.getAllPkgItems(fromSourceToApi, !fromSourceToApi); // deselect all targets for (PkgItem item : toItems) { item.setChecked(false); } // mark new one from the source for (PkgItem source : fromItems) { if (source.isChecked()) { // There should typically be a corresponding item in the target side for (PkgItem target : toItems) { if (target.isSameMainPackageAs(source.getMainPackage())) { target.setChecked(true); break; } } } } } /** * Synchronize the 'checked' state of PkgItems in the tree with their internal isChecked state. */ private void syncViewerSelection() { ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); Object input = mTreeViewer.getInput(); if (input == null) { return; } for (Object cat : provider.getElements(input)) { Object[] children = provider.getElements(cat); boolean allChecked = children.length > 0; for (Object child : children) { if (child instanceof PkgItem) { PkgItem item = (PkgItem) child; boolean checked = item.isChecked(); allChecked &= checked; if (checked != mTreeViewer.getChecked(item)) { if (checked) { if (!mTreeViewer.getExpandedState(cat)) { mTreeViewer.setExpandedState(cat, true); } } checkAndExpandItem(item, checked, true/*fixChildren*/, false/*fixParent*/); } } } if (allChecked != mTreeViewer.getChecked(cat)) { mTreeViewer.setChecked(cat, allChecked); } } } /** * Indicate an install/delete operation is pending. * This disables the install/delete buttons. * Use {@link #endOperationPending()} to revert, typically in a {@code try..finally} block. */ private void beginOperationPending() { mOperationPending = true; updateButtonsState(); } private void endOperationPending() { mOperationPending = false; updateButtonsState(); } /** * Updates the Install and Delete Package buttons. */ private void updateButtonsState() { int numPackages = getArchivesForInstall(null /*archives*/); mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending); mButtonInstall.setText(numPackages == 0 ? "Install packages..." : // disabled button case numPackages == 1 ? "Install 1 package..." : String.format("Install %d packages...", numPackages)); // We can only delete local archives numPackages = getArchivesToDelete(null /*outMsg*/, null /*outArchives*/); mButtonDelete.setEnabled((numPackages > 0) && !mOperationPending); mButtonDelete.setText(numPackages == 0 ? "Delete packages..." : // disabled button case numPackages == 1 ? "Delete 1 package..." : String.format("Delete %d packages...", numPackages)); } /** * Called when the Install Package button is selected. * Collects the packages to be installed and shows the installation window. */ private void onButtonInstall() { ArrayList<Archive> archives = new ArrayList<Archive>(); getArchivesForInstall(archives); if (mUpdaterData != null) { boolean needsRefresh = false; try { beginOperationPending(); List<Archive> installed = mUpdaterData.updateOrInstallAll_WithGUI(archives, mCheckFilterObsolete.getSelection() /* includeObsoletes */, mContext == SdkInvocationContext.IDE ? UpdaterData.TOOLS_MSG_UPDATED_FROM_ADT : UpdaterData.TOOLS_MSG_UPDATED_FROM_SDKMAN); needsRefresh = installed != null && !installed.isEmpty(); } finally { endOperationPending(); if (needsRefresh) { // The local package list has changed, make sure to refresh it localReload(); } } } } /** * Selects the archives that can be installed. * This can be used with a null {@code outArchives} just to count the number of * installable archives. * * @param outArchives An archive list where to add the archives that can be installed. * This can be null. * @return The number of archives that can be installed. */ private int getArchivesForInstall(List<Archive> outArchives) { if (mTreeViewer == null || mTreeViewer.getTree() == null || mTreeViewer.getTree().isDisposed()) { return 0; } Object[] checked = mTreeViewer.getCheckedElements(); if (checked == null) { return 0; } int count = 0; // Give us a way to force install of incompatible archives. boolean checkIsCompatible = System.getenv(ArchiveInstaller.ENV_VAR_IGNORE_COMPAT) == null; if (mDisplayArchives) { // In detail mode, we display archives so we can install only the // archives that are actually selected. for (Object c : checked) { if (c instanceof Archive) { Archive a = (Archive) c; if (a != null) { if (checkIsCompatible && !a.isCompatible()) { continue; } count++; if (outArchives != null) { outArchives.add((Archive) c); } } } } } else { // In non-detail mode, we install all the compatible archives // found in the selected pkg items. We also automatically // select update packages rather than the root package if any. for (Object c : checked) { Package p = null; if (c instanceof Package) { // This is an update package p = (Package) c; } else if (c instanceof PkgItem) { p = ((PkgItem) c).getMainPackage(); PkgItem pi = (PkgItem) c; if (pi.getState() == PkgState.INSTALLED) { // We don't allow installing items that are already installed // unless they have a pending update. p = pi.getUpdatePkg(); } else if (pi.getState() == PkgState.NEW) { p = pi.getMainPackage(); } } if (p != null) { for (Archive a : p.getArchives()) { if (a != null) { if (checkIsCompatible && !a.isCompatible()) { continue; } count++; if (outArchives != null) { outArchives.add(a); } } } } } } return count; } /** * Called when the Delete Package button is selected. * Collects the packages to be deleted, prompt the user for confirmation * and actually performs the deletion. */ private void onButtonDelete() { final String title = "Delete SDK Package"; StringBuilder msg = new StringBuilder("Are you sure you want to delete:"); // A list of archives to delete final ArrayList<Archive> archives = new ArrayList<Archive>(); getArchivesToDelete(msg, archives); if (!archives.isEmpty()) { msg.append("\n").append("This cannot be undone."); //$NON-NLS-1$ if (MessageDialog.openQuestion(getShell(), title, msg.toString())) { try { beginOperationPending(); mUpdaterData.getTaskFactory().start("Delete Package", new ITask() { public void run(ITaskMonitor monitor) { monitor.setProgressMax(archives.size() + 1); for (Archive a : archives) { monitor.setDescription("Deleting '%1$s' (%2$s)", a.getParentPackage().getShortDescription(), a.getLocalOsPath()); // Delete the actual package a.deleteLocal(); monitor.incProgress(1); if (monitor.isCancelRequested()) { break; } } monitor.incProgress(1); monitor.setDescription("Done"); } }); } finally { endOperationPending(); // The local package list has changed, make sure to refresh it localReload(); } } } } /** * Selects the archives that can be deleted and collect their names. * This can be used with a null {@code outArchives} and a null {@code outMsg} * just to count the number of archives to be deleted. * * @param outMsg A StringBuilder where the names of the packages to be deleted is * accumulated. This is used to confirm deletion with the user. * @param outArchives An archive list where to add the archives that can be installed. * This can be null. * @return The number of archives that can be deleted. */ private int getArchivesToDelete(StringBuilder outMsg, List<Archive> outArchives) { if (mTreeViewer == null || mTreeViewer.getTree() == null || mTreeViewer.getTree().isDisposed()) { return 0; } Object[] checked = mTreeViewer.getCheckedElements(); if (checked == null) { // This should not happen since the button should be disabled return 0; } int count = 0; if (mDisplayArchives) { // In detail mode, select archives that can be deleted for (Object c : checked) { if (c instanceof Archive) { Archive a = (Archive) c; if (a != null && a.isLocal()) { count++; if (outMsg != null) { String osPath = a.getLocalOsPath(); File dir = new File(osPath); Package p = a.getParentPackage(); if (p != null && dir.isDirectory()) { outMsg.append("\n - ") //$NON-NLS-1$ .append(p.getShortDescription()); } } if (outArchives != null) { outArchives.add(a); } } } } } else { // In non-detail mode, select archives of selected packages that can be deleted. for (Object c : checked) { if (c instanceof PkgItem) { PkgItem pi = (PkgItem) c; PkgState state = pi.getState(); if (state == PkgState.INSTALLED) { Package p = pi.getMainPackage(); for (Archive a : p.getArchives()) { if (a != null && a.isLocal()) { count++; if (outMsg != null) { String osPath = a.getLocalOsPath(); File dir = new File(osPath); if (dir.isDirectory()) { outMsg.append("\n - ") //$NON-NLS-1$ .append(p.getShortDescription()); } } if (outArchives != null) { outArchives.add(a); } } } } } } } return count; } // ---------------------- /** * A custom version of {@link TreeColumnViewerLabelProvider} which * handles {@link TreePath}s and delegates content to a base * {@link PkgCellLabelProvider} for the given {@link TreeViewerColumn}. * <p/> * The implementation handles a variety of providers (table label, table * color, table font) but does not implement a tooltip provider, so we * delegate the calls here to the appropriate {@link PkgCellLabelProvider}. * <p/> * Only {@link #getToolTipText(Object)} is really useful for us but we * delegate all the tooltip calls for completeness and avoid surprises later * if we ever decide to override more things in the label provider. */ public class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider { private CellLabelProvider mTooltipProvider; public PkgTreeColumnViewerLabelProvider(TreeViewerColumn column) { super(new PkgCellLabelProvider(column)); } @Override public void setProviders(Object provider) { super.setProviders(provider); if (provider instanceof CellLabelProvider) { mTooltipProvider = (CellLabelProvider) provider; } } @Override public Image getToolTipImage(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipImage(object); } return super.getToolTipImage(object); } @Override public String getToolTipText(Object element) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipText(element); } return super.getToolTipText(element); } @Override public Color getToolTipBackgroundColor(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipBackgroundColor(object); } return super.getToolTipBackgroundColor(object); } @Override public Color getToolTipForegroundColor(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipForegroundColor(object); } return super.getToolTipForegroundColor(object); } @Override public Font getToolTipFont(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipFont(object); } return super.getToolTipFont(object); } @Override public Point getToolTipShift(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipShift(object); } return super.getToolTipShift(object); } @Override public boolean useNativeToolTip(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.useNativeToolTip(object); } return super.useNativeToolTip(object); } @Override public int getToolTipTimeDisplayed(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipTimeDisplayed(object); } return super.getToolTipTimeDisplayed(object); } @Override public int getToolTipDisplayDelayTime(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipDisplayDelayTime(object); } return super.getToolTipDisplayDelayTime(object); } @Override public int getToolTipStyle(Object object) { if (mTooltipProvider != null) { return mTooltipProvider.getToolTipStyle(object); } return super.getToolTipStyle(object); } } public class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { private final TreeViewerColumn mColumn; public PkgCellLabelProvider(TreeViewerColumn column) { super(); mColumn = column; } @Override public String getText(Object element) { if (mColumn == mColumnName) { if (element instanceof PkgCategory) { return ((PkgCategory) element).getLabel(); } else if (element instanceof PkgItem) { return getPkgItemName((PkgItem) element); } else if (element instanceof IDescription) { return ((IDescription) element).getShortDescription(); } } else if (mColumn == mColumnApi) { int api = -1; if (element instanceof PkgItem) { api = ((PkgItem) element).getApi(); } if (api >= 1) { return Integer.toString(api); } } else if (mColumn == mColumnRevision) { if (element instanceof PkgItem) { PkgItem pkg = (PkgItem) element; return Integer.toString(pkg.getRevision()); } } else if (mColumn == mColumnStatus) { if (element instanceof PkgItem) { PkgItem pkg = (PkgItem) element; switch (pkg.getState()) { case INSTALLED: Package update = pkg.getUpdatePkg(); if (update != null) { return String.format("Update available: rev. %1$s", update.getRevision()); } return "Installed"; case NEW: Package p = pkg.getMainPackage(); if (p != null && p.hasCompatibleArchive()) { return "Not installed"; } else { return String.format("Not compatible with %1$s", SdkConstants.currentPlatformName()); } } return pkg.getState().toString(); } else if (element instanceof Package) { // This is an update package. return "New revision " + Integer.toString(((Package) element).getRevision()); } } return ""; } private String getPkgItemName(PkgItem item) { String name = item.getName().trim(); if (isSortByApi()) { // When sorting by API, the package name might contains the API number // or the platform name at the end. If we find it, cut it out since it's // redundant. PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); String apiLabel = cat.getApiLabel(); String platLabel = cat.getPlatformName(); if (platLabel != null && name.endsWith(platLabel)) { return name.substring(0, name.length() - platLabel.length()); } else if (apiLabel != null && name.endsWith(apiLabel)) { return name.substring(0, name.length() - apiLabel.length()); } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { // For obsolete items, the format is "<base name> <platform name> (Obsolete)" // so in this case only accept removing a platform name that is not at // the end. name = name.replace(platLabel, ""); //$NON-NLS-1$ } } // Collapse potential duplicated spacing name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ return name; } private PkgCategory findCategoryForItem(PkgItem item) { List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); for (PkgCategory cat : cats) { for (PkgItem i : cat.getItems()) { if (i == item) { return cat; } } } return null; } @Override public Image getImage(Object element) { ImageFactory imgFactory = mUpdaterData.getImageFactory(); if (imgFactory != null) { if (mColumn == mColumnName) { if (element instanceof PkgCategory) { return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); } else if (element instanceof PkgItem) { return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); } return imgFactory.getImageForObject(element); } else if (mColumn == mColumnStatus && element instanceof PkgItem) { PkgItem pi = (PkgItem) element; switch (pi.getState()) { case INSTALLED: if (pi.hasUpdatePkg()) { return imgFactory.getImageByName(ICON_PKG_UPDATE); } else { return imgFactory.getImageByName(ICON_PKG_INSTALLED); } case NEW: Package p = pi.getMainPackage(); if (p != null && p.hasCompatibleArchive()) { return imgFactory.getImageByName(ICON_PKG_NEW); } else { return imgFactory.getImageByName(ICON_PKG_INCOMPAT); } } } } return super.getImage(element); } // -- ITableFontProvider public Font getFont(Object element, int columnIndex) { if (element instanceof PkgItem) { if (((PkgItem) element).getState() == PkgState.NEW) { return mTreeFontItalic; } } else if (element instanceof Package) { // update package return mTreeFontItalic; } return super.getFont(element); } // -- Tooltip support @Override public String getToolTipText(Object element) { if (element instanceof PkgItem) { element = ((PkgItem) element).getMainPackage(); } if (element instanceof IDescription) { String s = ((IDescription) element).getLongDescription(); if (element instanceof Package) { SdkSource src = ((Package) element).getParentSource(); if (src != null) { try { URL url = new URL(src.getUrl()); String host = url.getHost(); if (((Package) element).isLocal()) { s += String.format("\nInstalled from %1$s", host); } else { s += String.format("\nProvided by %1$s", host); } } catch (MalformedURLException ignore) { } } } return s; } return super.getToolTipText(element); } @Override public Point getToolTipShift(Object object) { return new Point(15, 5); } @Override public int getToolTipDisplayDelayTime(Object object) { return 500; } } private class PkgContentProvider implements ITreeContentProvider { public Object[] getChildren(Object parentElement) { if (parentElement instanceof ArrayList<?>) { return ((ArrayList<?>) parentElement).toArray(); } else if (parentElement instanceof PkgCategory) { return ((PkgCategory) parentElement).getItems().toArray(); } else if (parentElement instanceof PkgItem) { if (mDisplayArchives) { Package pkg = ((PkgItem) parentElement).getUpdatePkg(); // Display update packages as sub-items if the details mode is activated. if (pkg != null) { return new Object[] { pkg }; } return ((PkgItem) parentElement).getArchives(); } } else if (parentElement instanceof Package) { if (mDisplayArchives) { return ((Package) parentElement).getArchives(); } } return new Object[0]; } @SuppressWarnings("unchecked") public Object getParent(Object element) { // This operation is expensive, so we do the minimum // and don't try to cover all cases. if (element instanceof PkgItem) { Object input = mTreeViewer.getInput(); if (input != null) { for (PkgCategory cat : (List<PkgCategory>) input) { if (cat.getItems().contains(element)) { return cat; } } } } return null; } public boolean hasChildren(Object parentElement) { if (parentElement instanceof ArrayList<?>) { return true; } else if (parentElement instanceof PkgCategory) { return true; } else if (parentElement instanceof PkgItem) { if (mDisplayArchives) { Package pkg = ((PkgItem) parentElement).getUpdatePkg(); // Display update packages as sub-items if the details mode is activated. if (pkg != null) { return true; } Archive[] archives = ((PkgItem) parentElement).getArchives(); return archives.length > 0; } } else if (parentElement instanceof Package) { if (mDisplayArchives) { return ((Package) parentElement).getArchives().length > 0; } } return false; } public Object[] getElements(Object inputElement) { return getChildren(inputElement); } public void dispose() { // unused } public void inputChanged(Viewer arg0, Object arg1, Object arg2) { // unused } } // --- Implementation of ISdkChangeListener --- public void onSdkLoaded() { onSdkReload(); } public void onSdkReload() { // The sdkmanager finished reloading its data. We must not call localReload() from here // since we don't want to alter the sdkmanager's data that just finished loading. loadPackages(); } public void preInstallHook() { // nothing to be done for now. } public void postInstallHook() { // nothing to be done for now. } // --- End of hiding from SWT Designer --- //$hide<<$ }