| 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; |
| } |
| } |
| } |