/*
* Copyright (C) 2013 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.tools.idea.sdk.wizard;
import com.android.sdklib.internal.repository.IDescription;
import com.android.sdklib.internal.repository.IListDescription;
import com.android.sdklib.repository.local.LocalPkgInfo;
import com.android.tools.idea.sdk.SdkState;
import com.android.tools.idea.wizard.TemplateWizardStep;
import com.intellij.icons.AllIcons;
import com.intellij.ide.wizard.CommitStepException;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.util.Disposer;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.table.JBTable;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import java.awt.*;
/**
* This page shows 2 things:
* - The list of all SDK packages installed and whether an update is available.
* - The list of all SDK packages available on remote server that the user could install.
*
* The UX workflow is that each package has a checkbox.
* All checked items are either installed or to be installed. A user can:
* - Uncheck a box to indicate a package needs to be removed.
* - Check a box to indicate a package needs to be installed.
*
* The output of the wizard is thus a list of actions (cf {@link Action}).
* - Install some new package
* - Update some existing package
* - Remove some existing package.
*
* Once the user hits Next, s/he ends up in the "Confirmation" page that displays
* the actual list of packages to install or remove. That list also includes
* transient dependencies needed to fulfill the user install choice, displays the
* package license and it has its own accept/refuse state for each package.
*
* The current UX workflow for updates: since the base package is already installed,
* the package is shown with a checked stated. By default it will be marked for
* updates. If the user doesn't want to update, the current model is for the user
* to do that in the *next* page (SmwConfirmationStep) by not de-selecting the update
* (so essentially it's an opt-out.)
* <p/>
*
* TODO: This wizard is a placeholder. It lacks several things:
* - Each item's display could be made much better. Ideally I'd like something like
* the IJ settings > Settings > Plugin > Browse Repository where some of the
* package attributes (version, API) are right-aligned with a smaller font.
* - We need a way to sort & filter the list using an ActionBar icon set at the top of the page:
* - Typical options would: display only what's installed, not installed, new & updates.
* - Have a boolean state to hide obsoleted packages by default.
* - Have a way to hide "older" Android releases by default. There should be an easy threshold
* set in constants somewhere so that we can easily adjust it (client side, not server side)
* and then hide APIs (say < 14) if they have nothing installed.
* Another option is to have 3 states (latest, common, all) where common would by default
* display APIs that match the android dashboard (e.g. 10 and >= 15).
* Also do something similar for build-tools so that we don't crowd that much the display.
* - The layout is open to discussion.
* - One model is to have a small text area at the bottom under the list that displays information
* on the selected package: source URL, long-description from the XML if present, download side,
* SHA1, etc. (For an example, see what the current SDK Manager does in the tooltip on a package.)
* - Another model is to do like the IJ > Settings > Plugin table and have a display on the right
* side. I think it's only justified if we have more than a couple lines of info to display, which
* right now is not the case.
* - The sdklib API used to retrieve package informtion does not currently provide the description
* meta-data from the remote XML. It needs to provide the description and the new list-display fields
* (this is pending another CL that adds the feature to sdklib.)
*/
public class SmwSelectionStep extends TemplateWizardStep implements Disposable {
private final SmwState myWizardState;
private JPanel myContentPanel;
private JLabel myTextDescription; // TODO: display details based on selection
private JBLabel myLabelSdkPath;
private JBTable myTable;
private JPanel myToolbarPanel;
private JLabel myErrorLabel;
private SmwSelectionTableModel myTableModel;
private boolean myInitOnce = true;
public SmwSelectionStep(@NotNull SmwState wizardState, @Nullable UpdateListener updateListener) {
super(wizardState, null /*project*/, null /*module*/, null /*sidePanelIcon*/, updateListener);
myWizardState = wizardState;
}
@Override
public void dispose() {
}
@Override
public JComponent getComponent() {
return myContentPanel;
}
private void createUIComponents() {
myTable = new SmwSelectionTable();
}
/**
* Called every time step becomes visible (even when using Previous to navigate back to the step).
* Fills in the SDK path, fills in the selection-step specific table/model and triggers an SDK update
* the first time.
*/
@Override
public void _init() {
if (!myInitOnce) {
return;
}
myInitOnce = false;
super._init();
SdkState sdkState = myWizardState.getSdkState();
if (sdkState != null) {
AndroidSdkData sdkData = sdkState.getSdkData();
//noinspection ConstantConditions
myLabelSdkPath.setText(sdkData.getLocalSdk().getLocation().getPath());
}
ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("SdkManager", getActionGroup(true), true);
final JComponent component = actionToolbar.getComponent();
myToolbarPanel.add(component, BorderLayout.WEST);
SmwSelectionTableModel.LabelColumnInfo pkgColumn = new SmwSelectionTableModel.LabelColumnInfo("Package");
SmwSelectionTableModel.InstallColumnInfo selColumn = new SmwSelectionTableModel.InstallColumnInfo("Installed");
myTableModel = new SmwSelectionTableModel(pkgColumn, selColumn);
Disposer.register(this, myTableModel);
pkgColumn.setModel(myTableModel);
selColumn.setModel(myTableModel);
myTable.setModel(myTableModel);
myTableModel.addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
// Invoked by the table model on the UI thread
SmwSelectionStep.this.update();
}
});
ListSelectionModel lsm = myTable.getSelectionModel();
lsm.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
onTableSelection(e);
}
});
onTableSelection(new ListSelectionEvent(lsm, lsm.getMinSelectionIndex(), lsm.getMaxSelectionIndex(), lsm.getValueIsAdjusting()));
if (sdkState != null) {
myTableModel.linkToSdkState(sdkState);
Runnable onSuccess = new Runnable() {
@Override
public void run() {
ListSelectionModel lsm = myTable.getSelectionModel();
onTableSelection(new ListSelectionEvent(lsm, lsm.getMinSelectionIndex(), lsm.getMaxSelectionIndex(), lsm.getValueIsAdjusting()));
}
};
sdkState.loadAsync(1000 * 60 * 10, // 10 minutes timeout since last check
false, // canBeCancelled
onSuccess, // onSuccess
null); // onError
}
}
@NotNull
@Override
protected JLabel getDescription() {
// We're not using the Description field of the Template Wizard Step here.
// Since nullable isn't supported, share it with the error label (which is
// fine since, again, template wizard description field isn't useful here.)
return myErrorLabel;
}
@NotNull
@Override
protected JLabel getError() {
return myErrorLabel;
}
/**
* Called when user presses "Next", "Previous" or "Finish" button.
* {@inheritDoc}
*/
@Override
public void _commit(boolean finishChosen) throws CommitStepException {
myWizardState.setSelectedActions(myTableModel.getActions());
super._commit(finishChosen);
}
@Override
public boolean validate() {
if (myTableModel != null && myTableModel.getActions().isEmpty()) {
return false;
}
return super.validate();
}
private void onTableSelection(ListSelectionEvent e) {
Object src = e.getSource();
if (!(src instanceof ListSelectionModel) || e.getValueIsAdjusting()) {
return;
}
ListSelectionModel lsm = (ListSelectionModel)src;
StringBuilder sb = new StringBuilder("<html>");
IListDescription item = lsm.isSelectionEmpty() ? null : myTableModel.getObjectAt(lsm.getMinSelectionIndex());
if (item == null) {
sb.append("Please select a package to see its details.");
} else {
if (item instanceof IDescription) {
sb.append(((IDescription)item).getLongDescription());
} else {
sb.append(item.getListDescription());
}
if (item instanceof LocalPkgInfo) {
LocalPkgInfo lpi = (LocalPkgInfo)item;
if (lpi.hasUpdate()) {
assert lpi.getUpdate() != null;
sb.append("\n\n").append(lpi.getUpdate().getLongDescription());
}
}
}
sb.append("</html>");
myTextDescription.setText(sb.toString().replace("\n", "<br/>\n"));
}
protected ActionGroup getActionGroup(boolean inToolbar) {
DefaultActionGroup actionGroup = new DefaultActionGroup();
actionGroup.add(new RefreshAction("Refresh", "Refresh list", AllIcons.Actions.Refresh));
actionGroup.add(Separator.getInstance());
actionGroup.add(new SdkStateNeededAction("Install", "Install item", AllIcons.Actions.Install));
actionGroup.add(new SdkStateNeededAction("Uninstall", "Uninstall item", AllIcons.Actions.Uninstall));
return actionGroup;
}
protected class SdkStateNeededAction extends DumbAwareAction {
public SdkStateNeededAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {
super(text, description, icon);
}
@Override
public void actionPerformed(AnActionEvent e) {
// no-op
}
@Override
public void update(AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setEnabled(myWizardState.getSdkState() != null);
}
}
protected class RefreshAction extends SdkStateNeededAction {
public RefreshAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {
super(text, description, icon);
}
@Override
public void actionPerformed(AnActionEvent e) {
SdkState sdkState = myWizardState.getSdkState();
if (sdkState != null) {
sdkState.loadAsync(0, // no delay, check now
false, // canBeCancelled
null, // onSuccess
null); // onError
}
}
}
}