/*
* 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.ui;
import com.android.sdklib.internal.repository.ITask;
import com.android.sdklib.internal.repository.ITaskMonitor;
import com.android.sdklib.internal.repository.archives.Archive;
import com.android.sdklib.internal.repository.archives.ArchiveInstaller;
import com.android.sdklib.internal.repository.packages.Package;
import com.android.sdkuilib.internal.repository.UpdaterData;
import com.android.sdkuilib.internal.repository.core.PkgCategory;
import com.android.sdkuilib.internal.repository.core.PkgCategoryApi;
import com.android.sdkuilib.internal.repository.core.PkgContentProvider;
import com.android.sdkuilib.internal.repository.core.PkgItem;
import com.android.sdkuilib.internal.repository.core.PkgItem.PkgState;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
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.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.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
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.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
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.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 final class PackagesPage extends Composite implements ISdkChangeListener {
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 PackagesPageImpl mImpl;
private final SdkInvocationContext mContext;
private boolean mDisplayArchives = false;
private boolean mOperationPending;
private Composite mGroupPackages;
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 Button mButtonDelete;
private Button mButtonInstall;
private Font mTreeFontItalic;
private TreeColumn mTreeColumnName;
private CheckboxTreeViewer mTreeViewer;
public PackagesPage(
Composite parent,
int swtStyle,
UpdaterData updaterData,
SdkInvocationContext context) {
super(parent, swtStyle);
mImpl = new PackagesPageImpl(updaterData) {
@Override
protected boolean isUiDisposed() {
return mGroupPackages == null || mGroupPackages.isDisposed();
};
@Override
protected void syncExec(Runnable runnable) {
if (!isUiDisposed()) {
mGroupPackages.getDisplay().syncExec(runnable);
}
};
@Override
protected void syncViewerSelection() {
PackagesPage.this.syncViewerSelection();
}
@Override
protected void refreshViewerInput() {
PackagesPage.this.refreshViewerInput();
}
@Override
protected boolean isSortByApi() {
return PackagesPage.this.isSortByApi();
}
@Override
protected Font getTreeFontItalic() {
return mTreeFontItalic;
}
@Override
protected void loadPackages(boolean useLocalCache, boolean overrideExisting) {
PackagesPage.this.loadPackages(useLocalCache, overrideExisting);
}
};
mContext = context;
createContents(this);
postCreate(); //$hide$
}
public void performFirstLoad() {
mImpl.performFirstLoad();
}
@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);
Group groupPackages = new Group(parent, SWT.NONE);
mGroupPackages = groupPackages;
GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2);
groupPackages.setText("Packages");
GridLayoutBuilder.create(groupPackages).columns(1);
mTreeViewer = new CheckboxTreeViewer(groupPackages, SWT.BORDER);
mImpl.setITreeViewer(new PackagesPageImpl.ICheckboxTreeViewer() {
@Override
public Object getInput() {
return mTreeViewer.getInput();
}
@Override
public void setInput(List<PkgCategory> cats) {
mTreeViewer.setInput(cats);
}
@Override
public void setContentProvider(PkgContentProvider pkgContentProvider) {
mTreeViewer.setContentProvider(pkgContentProvider);
}
@Override
public void refresh() {
mTreeViewer.refresh();
}
@Override
public Object[] getCheckedElements() {
return mTreeViewer.getCheckedElements();
}
});
mTreeViewer.addFilter(new ViewerFilter() {
@Override
public boolean select(Viewer viewer, Object parentElement, Object element) {
return filterViewerItem(element);
}
});
mTreeViewer.addCheckStateListener(new ICheckStateListener() {
@Override
public void checkStateChanged(CheckStateChangedEvent event) {
onTreeCheckStateChanged(event); //$hide$
}
});
mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
@Override
public void doubleClick(DoubleClickEvent event) {
onTreeDoubleClick(event); //$hide$
}
});
Tree tree = mTreeViewer.getTree();
tree.setLinesVisible(true);
tree.setHeaderVisible(true);
GridDataBuilder.create(tree).fill().grab();
// column name icon is set when loading depending on the current filter type
// (e.g. API level or source)
TreeViewerColumn columnName = new TreeViewerColumn(mTreeViewer, SWT.NONE);
mTreeColumnName = columnName.getColumn();
mTreeColumnName.setText("Name");
mTreeColumnName.setWidth(340);
TreeViewerColumn columnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE);
TreeColumn treeColumn2 = columnApi.getColumn();
treeColumn2.setText("API");
treeColumn2.setAlignment(SWT.CENTER);
treeColumn2.setWidth(50);
TreeViewerColumn columnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE);
TreeColumn treeColumn3 = columnRevision.getColumn();
treeColumn3.setText("Rev.");
treeColumn3.setToolTipText("Revision currently installed");
treeColumn3.setAlignment(SWT.CENTER);
treeColumn3.setWidth(50);
TreeViewerColumn columnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE);
TreeColumn treeColumn4 = columnStatus.getColumn();
treeColumn4.setText("Status");
treeColumn4.setAlignment(SWT.LEAD);
treeColumn4.setWidth(190);
mImpl.setIColumns(
wrapColumn(columnName),
wrapColumn(columnApi),
wrapColumn(columnRevision),
wrapColumn(columnStatus));
mGroupOptions = new Composite(groupPackages, SWT.NONE);
GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab();
GridLayoutBuilder.create(mGroupOptions).columns(7).noMargins();
// Options line 1, 7 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();
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*/);
}
});
// placeholder between "select all" and "install"
Label placeholder = new Label(mGroupOptions, SWT.NONE);
GridDataBuilder.create(placeholder).hFill().hGrab();
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).vCenter().wHint(150);
mButtonInstall.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onButtonInstall(); //$hide$
}
});
// Options line 2, 7 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();
}
}
});
// placeholder between "repository" and "deselect"
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();
linkDeselect.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
super.widgetSelected(e);
onDeselectAll();
}
});
// placeholder between "deselect" and "delete"
placeholder = new Label(mGroupOptions, SWT.NONE);
GridDataBuilder.create(placeholder).hFill().hGrab();
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).vCenter().wHint(150);
mButtonDelete.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onButtonDelete(); //$hide$
}
});
}
private PackagesPageImpl.ITreeViewerColumn wrapColumn(final TreeViewerColumn column) {
return new PackagesPageImpl.ITreeViewerColumn() {
@Override
public void setLabelProvider(ColumnLabelProvider labelProvider) {
column.setLabelProvider(labelProvider);
}
};
}
private Image getImage(String filename) {
if (mImpl.mUpdaterData != null) {
ImageFactory imgFactory = mImpl.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:
mImpl.fullReload();
break;
case SHOW_ADDON_SITES:
AddonSitesDialog d = new AddonSitesDialog(getShell(), mImpl.mUpdaterData);
if (d.open()) {
mImpl.loadPackages();
}
break;
case TOGGLE_SHOW_ARCHIVES:
mDisplayArchives = !mDisplayArchives;
// Force the viewer to be refreshed
((PkgContentProvider) mTreeViewer.getContentProvider()).
setDisplayArchives(mDisplayArchives);
mTreeViewer.setInput(null);
refreshViewerInput();
syncViewerSelection();
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();
}
if (!item.isDisposed()) {
item.setSelection(value);
}
}
}
private void postCreate() {
mImpl.postCreate();
if (mImpl.mUpdaterData != null) {
mTextSdkOsPath.setText(mImpl.mUpdaterData.getOsSdkRoot());
}
((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives(
mDisplayArchives);
ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE);
Tree tree = mTreeViewer.getTree();
FontData fontData = tree.getFont().getFontData()[0];
fontData.setStyle(SWT.ITALIC);
mTreeFontItalic = new Font(tree.getDisplay(), fontData);
tree.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
mTreeFontItalic.dispose();
mTreeFontItalic = null;
}
});
}
private void loadPackages(boolean useLocalCache, boolean overrideExisting) {
if (mImpl.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.
boolean displaySortByApi = isSortByApi();
if (mTreeColumnName.isDisposed()) {
// If the UI got disposed, don't try to load anything since we won't be
// able to display it anyway.
return;
}
mTreeColumnName.setImage(getImage(
displaySortByApi ? PackagesPageIcons.ICON_SORT_BY_API
: PackagesPageIcons.ICON_SORT_BY_SOURCE));
mImpl.loadPackagesImpl(useLocalCache, overrideExisting);
}
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()) {
try {
mImpl.setViewerInput();
} catch (Exception ignore) {}
// 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()) {
boolean enablePreviews =
mImpl.mUpdaterData.getSettingsController().getSettings().getEnablePreviews();
mTreeViewer.setExpandedState(elem, true);
nextCategory: for (Object pkg :
((ITreeContentProvider) mTreeViewer.getContentProvider()).
getChildren(elem)) {
if (pkg instanceof PkgCategory) {
PkgCategory cat = (PkgCategory) pkg;
// Always expand the Tools category (and the preview one, if enabled)
if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS) ||
(enablePreviews &&
cat.getKey().equals(PkgCategoryApi.KEY_TOOLS_PREVIEW))) {
expandInitial(pkg);
continue nextCategory;
}
for (PkgItem item : cat.getItems()) {
if (item.getState() == PkgState.INSTALLED) {
expandInitial(pkg);
continue nextCategory;
}
}
}
}
}
}
/**
* 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 will update the tree's "selected" state and then invoke syncViewerSelection()
// which will in turn update tree.
mImpl.onSelectNewUpdates(selectNew, selectUpdates, selectTop);
}
/**
* Deselect all checked PkgItems.
*/
private void onDeselectAll() {
// This does not update the tree itself, syncViewerSelection does it below.
mImpl.onDeselectAll();
syncViewerSelection();
}
/**
* 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 =
mImpl.mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi);
List<PkgItem> toItems =
mImpl.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) {
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);
}
}
}
updateButtonsState();
}
/**
* 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() {
if (!mButtonInstall.isDisposed()) {
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));
}
if (!mButtonDelete.isDisposed()) {
// We can only delete local archives
int 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 (mImpl.mUpdaterData != null) {
boolean needsRefresh = false;
try {
beginOperationPending();
List<Archive> installed = mImpl.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
mImpl.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();
mImpl.mUpdaterData.getTaskFactory().start("Delete Package", new ITask() {
@Override
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
mImpl.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;
}
// ----------------------
// --- Implementation of ISdkChangeListener ---
@Override
public void onSdkLoaded() {
onSdkReload();
}
@Override
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.
mImpl.loadPackages();
}
@Override
public void preInstallHook() {
// nothing to be done for now.
}
@Override
public void postInstallHook() {
// nothing to be done for now.
}
// --- End of hiding from SWT Designer ---
//$hide<<$
}