/*
* Copyright (C) 2014 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.annotations.NonNull;
import com.android.sdklib.internal.repository.IListDescription;
import com.android.sdklib.repository.local.LocalPkgInfo;
import com.android.sdklib.repository.local.UpdateResult;
import com.android.sdklib.repository.remote.RemotePkgInfo;
import com.android.tools.idea.sdk.SdkLifecycleListener;
import com.android.tools.idea.sdk.SdkState;
import com.android.utils.Pair;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.ui.BooleanTableCellEditor;
import com.intellij.ui.BooleanTableCellRenderer;
import com.intellij.util.ui.ColumnInfo;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* Model for Selection step table.
* <p/>
* The objects handled by this table are either {@link LocalPkgInfo} or {@link RemotePkgInfo}.
* <p/>
* Implementation detail: Implements {@link Disposable} in order to drop the message bus connection
* automatically when disposed, and also do prevent in-flight async updates after disposal.
* <p/>
* Inspiration source: PluginTableModel.
*/
public class SmwSelectionTableModel extends AbstractTableModel implements Disposable {
@NonNull private final List<IListDescription> myInfos = new ArrayList<IListDescription>();
@NonNull private final Set<IListDescription> myChanged = new HashSet<IListDescription>();
private boolean myIsDisposed;
private final ColumnInfo[] myColumns;
public SmwSelectionTableModel(@NonNull ColumnInfo... columns) {
myColumns = columns;
}
@Override
public int getRowCount() {
return myInfos.size();
}
@Override
public int getColumnCount() {
return myColumns.length;
}
/**
* Returns the {@link ColumnInfo} for the given column index.
*
* @param columnIndex An index 0..size of columns-1.
* @return A non-null column info as given to the constructor.
* @throws java.lang.ArrayIndexOutOfBoundsException if {@code columnIndex} is invalid.
*/
@NonNull
public ColumnInfo getColumnInfo(int columnIndex) {
return myColumns[columnIndex];
}
/**
* Returns the object at the give row or null if the index is out of bounds.
* <p/>
* The objects handled by this table are either {@link LocalPkgInfo} or {@link RemotePkgInfo}.
*
* @param rowIndex A row index 0..numbers of rows-1.
* @return A row object or null if {@code rowIndex} is invalid.
*/
@Nullable
public IListDescription getObjectAt(int rowIndex) {
if (rowIndex < 0 || rowIndex >= myInfos.size()) {
return null;
}
return myInfos.get(rowIndex);
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
//noinspection ConstantConditions,unchecked
return myColumns[columnIndex].valueOf(myInfos.get(rowIndex));
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
//noinspection unchecked
return myColumns[columnIndex].isCellEditable(myInfos.get(rowIndex));
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
//noinspection unchecked
myColumns[columnIndex].setValue(myInfos.get(rowIndex), aValue);
for (int i = 0; i < myColumns.length; i++) {
fireTableCellUpdated(rowIndex, i);
}
}
public void linkToSdkState(@NonNull final SdkState sdkState) {
final Runnable resetInUiThread = new Runnable() {
@Override
public void run() {
if (!myIsDisposed) {
resetTable(sdkState);
}
}
};
resetInUiThread.run();
final ApplicationEx app = ApplicationManagerEx.getApplicationEx();
app.getMessageBus().connect(this).subscribe(SdkLifecycleListener.TOPIC, new SdkLifecycleListener() {
@Override
public void localSdkLoaded(@NonNull AndroidSdkData sdkData) {
// Only update if the exact same SDK data has changed
if (sdkState.getSdkData() == sdkData) {
// TODO only update local pkg infos that have changed
// Right now just reset the table model from scratch.
app.invokeLater(resetInUiThread, ModalityState.any());
}
}
@Override
public void remoteSdkLoaded(@NonNull AndroidSdkData sdkData) {
// Skip. Just wait for updatesComputed.
}
@Override
public void updatesComputed(@NonNull AndroidSdkData sdkData) {
// Only update if the exact same SDK data has changed
if (sdkState.getSdkData() == sdkData) {
// TODO only update updates pkg infos that have changed.
// Right now just reset the table model from scratch.
app.invokeLater(resetInUiThread, ModalityState.any());
}
}
});
}
private void resetTable(SdkState sdkState) {
myInfos.clear();
myChanged.clear();
Collections.addAll(myInfos, sdkState.getLocalPkgInfos());
UpdateResult updates = sdkState.getUpdates();
if (updates != null) {
myInfos.addAll(updates.getNewPkgs());
}
fireTableDataChanged();
}
private SmwSelectionAction computeAction(IListDescription item) {
boolean changed = myChanged.contains(item);
if (item instanceof LocalPkgInfo) {
LocalPkgInfo local = (LocalPkgInfo)item;
if (changed) {
return SmwSelectionAction.REMOVE;
}
if (local.hasUpdate()) {
return SmwSelectionAction.UPDATE;
}
} else if (item instanceof RemotePkgInfo) {
if (changed) {
return SmwSelectionAction.INSTALL;
} else {
return SmwSelectionAction.NEW_REMOTE;
}
}
return SmwSelectionAction.KEEP_LOCAL;
}
public List<Pair<SmwSelectionAction, IListDescription>> getActions() {
List<Pair<SmwSelectionAction, IListDescription>> actions =
new ArrayList<Pair<SmwSelectionAction, IListDescription>>(myChanged.size());
for (IListDescription item : myChanged) {
SmwSelectionAction action = computeAction(item);
switch (action) {
case INSTALL:
case UPDATE:
case REMOVE:
actions.add(Pair.of(action, item));
default:
// no-op for keep / new remote (shouldn't be in the list anyway)
}
}
return actions;
}
@Override
public void dispose() {
myIsDisposed = true;
}
// ------------
public static class LabelColumnInfo extends ColumnInfo<IListDescription, String> {
private SmwSelectionTableModel myModel;
public LabelColumnInfo(@NonNull String name) {
super(name);
}
public void setModel(@NonNull SmwSelectionTableModel model) {
myModel = model;
}
@Override
public String valueOf(IListDescription item) {
// TODO edit display text to something more user-friendly. This is just a placeholder.
String desc = desc = item.getListDescription();
SmwSelectionAction action = myModel == null ? null : myModel.computeAction(item);
if (action != null) {
if (action == SmwSelectionAction.INSTALL) {
desc = "[Install] " + desc;
} else if (action == SmwSelectionAction.REMOVE) {
desc = "[Remove] " + desc;
} else if (action == SmwSelectionAction.UPDATE) {
assert item instanceof LocalPkgInfo;
assert ((LocalPkgInfo) item).getUpdate() != null;
//noinspection ConstantConditions
desc = "[Update] " + ((LocalPkgInfo) item).getUpdate().getListDescription();
}
}
return desc;
}
@Override
public boolean isCellEditable(IListDescription item) {
return false;
}
@Nullable
@Override
public TableCellRenderer getRenderer(IListDescription item) {
return new DefaultTableCellRenderer();
}
}
// ------------
public static class InstallColumnInfo extends ColumnInfo<IListDescription, Boolean> {
private SmwSelectionTableModel myModel;
public InstallColumnInfo(@NonNull String name) {
super(name);
}
public void setModel(@NonNull SmwSelectionTableModel model) {
myModel = model;
}
private boolean isChanged(IListDescription item) {
return myModel != null && myModel.myChanged.contains(item);
}
@Override
public Boolean valueOf(IListDescription item) {
boolean installed = item instanceof LocalPkgInfo;
if (isChanged(item)) {
installed = !installed;
}
return installed;
}
@Override
public boolean isCellEditable(IListDescription item) {
return item instanceof LocalPkgInfo || item instanceof RemotePkgInfo;
}
@Override
public Class getColumnClass() {
return Boolean.class;
}
@Override
public TableCellEditor getEditor(IListDescription item) {
return new BooleanTableCellEditor();
}
@Nullable
@Override
public TableCellRenderer getRenderer(IListDescription o) {
return new BooleanTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row,
int column) {
return super.getTableCellRendererComponent(table, value == null ? Boolean.TRUE : value, isSelected, hasFocus, row, column);
}
};
}
@Override
public int getWidth(JTable table) {
return new JCheckBox().getPreferredSize().width;
}
@Override
public void setValue(IListDescription item, Boolean value) {
if (myModel != null) {
boolean installed = item instanceof LocalPkgInfo;
if (value.booleanValue() != installed) {
myModel.myChanged.add(item);
} else {
myModel.myChanged.remove(item);
}
}
super.setValue(item, value);
}
}
}