blob: 749dcc2fc350df84e3ba8c3875d7a9652334ecee [file] [log] [blame]
package com.intellij.updater;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class SwingUpdaterUI implements UpdaterUI {
static final int RESULT_REQUIRES_RESTART = 42;
private static final EmptyBorder FRAME_BORDER = new EmptyBorder(8, 8, 8, 8);
private static final EmptyBorder LABEL_BORDER = new EmptyBorder(0, 0, 5, 0);
private static final EmptyBorder BUTTONS_BORDER = new EmptyBorder(5, 0, 0, 0);
private static final String TITLE = "Update";
private static final String CANCEL_BUTTON_TITLE = "Cancel";
private static final String EXIT_BUTTON_TITLE = "Exit";
private static final String PROCEED_BUTTON_TITLE = "Proceed";
private final int mySuccessExitCode;
private final InstallOperation myOperation;
private final JLabel myProcessTitle;
private final JProgressBar myProcessProgress;
private final JLabel myProcessStatus;
private final JTextArea myConsole;
private final JPanel myConsolePane;
private final JButton myRetryButton;
private final JButton myCancelButton;
private final ConcurrentLinkedQueue<UpdateRequest> myQueue = new ConcurrentLinkedQueue<UpdateRequest>();
private final AtomicBoolean isCancelled = new AtomicBoolean(false);
private final AtomicBoolean isRunning = new AtomicBoolean(false);
private final AtomicBoolean hasError = new AtomicBoolean(false);
private final AtomicBoolean hasRetry = new AtomicBoolean(false);
private final JFrame myFrame;
private boolean myApplied;
/**
* Displays the updater UI and asynchronously runs the operation list.
*
* @param oldBuildDesc The old build description, for display purposes.
* @param newBuildDesc The new build description, for display purposes.
* @param successExitCode The desired exit code on success. Default is {@link #RESULT_REQUIRES_RESTART}.
* @param operation The install operations to perform.
*/
public SwingUpdaterUI(String oldBuildDesc,
String newBuildDesc,
int successExitCode,
InstallOperation operation) {
mySuccessExitCode = successExitCode;
myOperation = operation;
myProcessTitle = new JLabel(" ");
myProcessProgress = new JProgressBar(0, 100);
myProcessStatus = new JLabel(" ");
myCancelButton = new JButton(CANCEL_BUTTON_TITLE);
myRetryButton = new JButton("Retry");
myRetryButton.setEnabled(false);
myRetryButton.setVisible(false);
myConsole = new JTextArea();
myConsole.setLineWrap(true);
myConsole.setWrapStyleWord(true);
myConsole.setCaretPosition(myConsole.getText().length());
myConsole.setTabSize(1);
myConsole.setMargin(new Insets(2, 4, 2, 4));
myConsolePane = new JPanel(new BorderLayout());
myConsolePane.add(new JScrollPane(myConsole));
myConsolePane.setBorder(BUTTONS_BORDER);
myConsolePane.setVisible(false);
myCancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
doCancel();
}
});
myRetryButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
doRetry();
}
});
myFrame = new JFrame();
myFrame.setTitle(TITLE);
myFrame.setLayout(new BorderLayout());
myFrame.getRootPane().setBorder(FRAME_BORDER);
myFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
myFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
doCancel();
}
});
JPanel processPanel = new JPanel();
processPanel.setLayout(new BoxLayout(processPanel, BoxLayout.Y_AXIS));
processPanel.add(myProcessTitle);
processPanel.add(myProcessProgress);
processPanel.add(myProcessStatus);
processPanel.add(myConsolePane);
for (Component each : processPanel.getComponents()) {
((JComponent)each).setAlignmentX(Component.LEFT_ALIGNMENT);
}
JPanel buttonsPanel = new JPanel();
buttonsPanel.setBorder(BUTTONS_BORDER);
buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
buttonsPanel.add(Box.createHorizontalGlue());
buttonsPanel.add(myRetryButton);
buttonsPanel.add(myCancelButton);
myProcessTitle.setText("<html>Updating " + oldBuildDesc + " to " + newBuildDesc + "...");
myFrame.add(processPanel, BorderLayout.CENTER);
myFrame.add(buttonsPanel, BorderLayout.SOUTH);
myFrame.setMinimumSize(new Dimension(500, 50));
myFrame.pack();
myFrame.setLocationRelativeTo(null);
myFrame.setVisible(true);
myQueue.add(new UpdateRequest() {
@Override
public void perform() {
doPerform();
}
});
startRequestDispatching();
}
private void startRequestDispatching() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
Runner.printStackTrace(e);
return;
}
final List<UpdateRequest> pendingRequests = new ArrayList<UpdateRequest>();
UpdateRequest request;
while ((request = myQueue.poll()) != null) {
pendingRequests.add(request);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
for (UpdateRequest each : pendingRequests) {
each.perform();
}
}
});
}
}
}).start();
}
private void doCancel() {
if (isRunning.get()) {
int result = JOptionPane.showConfirmDialog(myFrame,
"The patch has not been applied yet.\nAre you sure you want to abort the operation?",
TITLE, JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.YES_OPTION) {
isCancelled.set(true);
myCancelButton.setEnabled(false);
}
}
else {
exit();
}
}
private void doRetry() {
hasError.set(false);
hasRetry.set(false);
isCancelled.set(false);
myQueue.add(new UpdateRequest() {
@Override
public void perform() {
myConsole.setText("");
myConsolePane.setVisible(false);
myConsolePane.setPreferredSize(new Dimension(10, 200));
myRetryButton.setEnabled(false);
myCancelButton.setEnabled(true);
}
});
doPerform();
}
private void doPerform() {
isRunning.set(true);
new Thread(new Runnable() {
@Override
public void run() {
try {
myApplied = myOperation.execute(SwingUpdaterUI.this);
}
catch (OperationCancelledException ignore) {
Runner.printStackTrace(ignore);
}
catch(Throwable e) {
Runner.printStackTrace(e);
showError(e);
}
finally {
isRunning.set(false);
if (hasRetry.get()) {
myRetryButton.setVisible(true);
myRetryButton.setEnabled(true);
}
if (hasError.get()) {
startProcess("Failed to apply patch");
setProgress(100);
myCancelButton.setText(EXIT_BUTTON_TITLE);
myCancelButton.setEnabled(true);
} else {
exit();
}
}
}
}).start();
}
private void exit() {
System.exit(myApplied ? mySuccessExitCode : 0);
}
@Override
public Map<String, ValidationResult.Option> askUser(final List<ValidationResult> validationResults) throws OperationCancelledException {
if (validationResults.isEmpty()) return Collections.emptyMap();
final Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>();
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
final JDialog dialog = new JDialog(myFrame, TITLE, true);
dialog.setLayout(new BorderLayout());
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
JPanel buttonsPanel = new JPanel();
buttonsPanel.setBorder(BUTTONS_BORDER);
buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
buttonsPanel.add(Box.createHorizontalGlue());
JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE);
proceedButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dialog.setVisible(false);
}
});
JButton cancelButton = new JButton(CANCEL_BUTTON_TITLE);
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
isCancelled.set(true);
myCancelButton.setEnabled(false);
dialog.setVisible(false);
}
});
buttonsPanel.add(proceedButton);
buttonsPanel.add(cancelButton);
dialog.getRootPane().setDefaultButton(proceedButton);
JTable table = new JTable();
table.setCellSelectionEnabled(true);
table.setDefaultEditor(ValidationResult.Option.class, new MyCellEditor());
table.setDefaultRenderer(Object.class, new MyCellRenderer());
MyTableModel model = new MyTableModel(validationResults);
table.setModel(model);
for (int i = 0; i < table.getColumnModel().getColumnCount(); i++) {
TableColumn each = table.getColumnModel().getColumn(i);
each.setPreferredWidth(MyTableModel.getColumnWidth(i, new Dimension(600, 400).width));
}
String message = "<html>There are some conflicts found in the installation.<br><br>" +
"Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] +
" column and press " + PROCEED_BUTTON_TITLE + ".<br>" +
"If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + ".</html>";
JLabel label = new JLabel(message);
label.setBorder(LABEL_BORDER);
dialog.add(label, BorderLayout.NORTH);
dialog.add(new JScrollPane(table), BorderLayout.CENTER);
dialog.add(buttonsPanel, BorderLayout.SOUTH);
dialog.getRootPane().setBorder(FRAME_BORDER);
dialog.setSize(new Dimension(600, 400));
dialog.setLocationRelativeTo(null);
dialog.setVisible(true);
result.putAll(model.getResult());
}
});
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
checkCancelled();
return result;
}
@Override
public void startProcess(final String title) {
myQueue.add(new UpdateRequest() {
@Override
public void perform() {
myProcessStatus.setText(title);
myProcessProgress.setIndeterminate(false);
myProcessProgress.setValue(0);
}
});
}
@Override
public void setProgress(final int percentage) {
myQueue.add(new UpdateRequest() {
@Override
public void perform() {
myProcessProgress.setIndeterminate(false);
myProcessProgress.setValue(percentage);
}
});
}
@Override
public void setProgressIndeterminate() {
myQueue.add(new UpdateRequest() {
@Override
public void perform() {
myProcessProgress.setIndeterminate(true);
}
});
}
@Override
public void setStatus(final String status) {
}
@Override
public void showError(final Throwable e) {
hasError.set(true);
StringWriter w = new StringWriter();
if (e instanceof RetryException) {
hasRetry.set(true);
w.write("+----------------\n");
w.write("| A file operation failed.\n");
w.write("| This might be due to a file being locked by another\n");
w.write("| application. Please try closing any application\n");
w.write("| that uses the files being updated then press 'Retry'.\n");
w.write("+----------------\n");
w.write("\n\n");
}
e.printStackTrace(new PrintWriter(w));
final String content = w.getBuffer().toString();
myQueue.add(new UpdateRequest() {
@Override
public void perform() {
StringWriter w = new StringWriter();
if (!myConsolePane.isVisible()) {
w.write("Temp. directory: ");
w.write(System.getProperty("java.io.tmpdir"));
w.write("\n\n");
}
myConsole.append(w.getBuffer().toString());
myConsole.append(content);
if (!myConsolePane.isVisible()) {
myConsole.setCaretPosition(0);
myConsolePane.setVisible(true);
myConsolePane.setPreferredSize(new Dimension(10, 200));
myFrame.pack();
}
}
});
}
@Override
public void checkCancelled() throws OperationCancelledException {
if (isCancelled.get()) throw new OperationCancelledException();
}
public interface InstallOperation {
boolean execute(UpdaterUI ui) throws OperationCancelledException;
}
private interface UpdateRequest {
void perform();
}
public static void main(String[] args) {
new SwingUpdaterUI("xxx", "yyy", RESULT_REQUIRES_RESTART, new InstallOperation() {
@Override
public boolean execute(UpdaterUI ui) throws OperationCancelledException {
ui.startProcess("Process1");
ui.checkCancelled();
for (int i = 0; i < 200; i++) {
ui.setStatus("i = " + i);
ui.checkCancelled();
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
ui.setProgress((i + 1) * 100 / 200);
}
ui.showError(new Throwable());
ui.startProcess("Process3");
ui.checkCancelled();
ui.setProgressIndeterminate();
try {
for (int i = 0; i < 200; i++) {
ui.setStatus("i = " + i);
ui.checkCancelled();
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
ui.setProgress((i + 1) * 100 / 200);
if (i == 100) {
List<ValidationResult> vr = new ArrayList<ValidationResult>();
vr.add(new ValidationResult(ValidationResult.Kind.ERROR,
"foo/bar",
ValidationResult.Action.CREATE,
"Hello",
ValidationResult.Option.REPLACE,
ValidationResult.Option.KEEP));
vr.add(new ValidationResult(ValidationResult.Kind.CONFLICT,
"foo/bar/baz",
ValidationResult.Action.DELETE,
"World",
ValidationResult.Option.DELETE,
ValidationResult.Option.KEEP));
vr.add(new ValidationResult(ValidationResult.Kind.INFO,
"xxx",
ValidationResult.Action.NO_ACTION,
"bla-bla",
ValidationResult.Option.IGNORE));
ui.askUser(vr);
}
}
}
finally {
ui.startProcess("Process2");
for (int i = 0; i < 200; i++) {
ui.setStatus("i = " + i);
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
ui.setProgress((i + 1) * 100 / 200);
}
}
return true;
}
});
}
private static class MyTableModel extends AbstractTableModel {
public static final String[] COLUMNS = new String[]{"File", "Action", "Problem", "Solution"};
public static final int OPTIONS_COLUMN_INDEX = 3;
private final List<Item> myItems = new ArrayList<Item>();
public MyTableModel(List<ValidationResult> validationResults) {
for (ValidationResult each : validationResults) {
myItems.add(new Item(each, each.options.get(0)));
}
}
@Override
public int getColumnCount() {
return COLUMNS.length;
}
@Override
public String getColumnName(int column) {
return COLUMNS[column];
}
public static int getColumnWidth(int column, int totalWidth) {
switch (column) {
case 0:
return (int)(totalWidth * 0.6);
default:
return (int)(totalWidth * 0.15);
}
}
@Override
public Class<?> getColumnClass(int columnIndex) {
if (columnIndex == OPTIONS_COLUMN_INDEX) {
return ValidationResult.Option.class;
}
return super.getColumnClass(columnIndex);
}
@Override
public int getRowCount() {
return myItems.size();
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return columnIndex == OPTIONS_COLUMN_INDEX && getOptions(rowIndex).size() > 1;
}
@Override
public void setValueAt(Object value, int rowIndex, int columnIndex) {
if (columnIndex == OPTIONS_COLUMN_INDEX) {
myItems.get(rowIndex).option = (ValidationResult.Option)value;
}
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
Item item = myItems.get(rowIndex);
switch (columnIndex) {
case 0:
return item.validationResult.path;
case 1:
return item.validationResult.action;
case 2:
return item.validationResult.message;
case OPTIONS_COLUMN_INDEX:
return item.option;
}
return null;
}
public ValidationResult.Kind getKind(int rowIndex) {
return myItems.get(rowIndex).validationResult.kind;
}
public List<ValidationResult.Option> getOptions(int rowIndex) {
Item item = myItems.get(rowIndex);
return item.validationResult.options;
}
public Map<String, ValidationResult.Option> getResult() {
Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>();
for (Item each : myItems) {
result.put(each.validationResult.path, each.option);
}
return result;
}
private static class Item {
ValidationResult validationResult;
ValidationResult.Option option;
private Item(ValidationResult validationResult, ValidationResult.Option option) {
this.validationResult = validationResult;
this.option = option;
}
}
}
private static class MyCellEditor extends DefaultCellEditor {
public MyCellEditor() {
super(new JComboBox());
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
MyTableModel tableModel = (MyTableModel)table.getModel();
DefaultComboBoxModel comboModel = new DefaultComboBoxModel();
for (ValidationResult.Option each : tableModel.getOptions(row)) {
comboModel.addElement(each);
}
((JComboBox)editorComponent).setModel(comboModel);
return super.getTableCellEditorComponent(table, value, isSelected, row, column);
}
}
private static class MyCellRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (!isSelected) {
MyTableModel tableModel = (MyTableModel)table.getModel();
Color color = table.getBackground();
switch (tableModel.getKind(row)) {
case ERROR:
color = new Color(255, 175, 175);
break;
case CONFLICT:
color = new Color(255, 240, 240);
break;
}
result.setBackground(color);
}
return result;
}
}
}