package net.sourceforge.transfile.ui.swing; import static net.sourceforge.transfile.ui.swing.GUITools.rollover; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.Image; import java.awt.Insets; import java.awt.RenderingHints; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import net.sourceforge.transfile.operations.Operation; import net.sourceforge.transfile.operations.Operation.Controller; /** * * @author codistmonk (creation 2010-05-20) * */ public class OperationComponent extends JPanel { private final SelectionModel selectionModel; private final Operation operation; /** * * @param selectionModel * <br>Can be null * <br>Shared parameter * @param operation * <br>Should not be null * <br>Shared parameter */ public OperationComponent(final SelectionModel selectionModel, final Operation operation) { this.selectionModel = selectionModel; this.operation = operation; this.addMouseListener(new MouseAdapter() { @Override public final void mousePressed(final MouseEvent event) { OperationComponent.this.select(); } }); this.setup(); } /** * * @return * <br>A non-null value * <br>A shared value */ public final Operation getOperation() { return this.operation; } @Override public final void paint(final Graphics graphics) { if (this.isSelected()) { this.setBackground(Color.BLUE); } else if (this.getParent() != null) { final int z = this.getParent().getComponentZOrder(this); this.setBackground(z % 2 == 0 ? DEFAULT_BACKGROUND_COLOR : ALTERNATE_BACKGROUND_COLOR); } else { this.setBackground(DEFAULT_BACKGROUND_COLOR); } super.paint(graphics); } /** * Does nothing if the selection model is null. */ public final void deselect() { if (this.selectionModel != null) { this.selectionModel.setSelection(null); } } /** * Does nothing if the selection model is null. */ public final void select() { if (this.selectionModel != null) { this.selectionModel.setSelection(this); if (this.getParent() != null) { this.getParent().repaint(); } } } /** * * @return {@code true} if the selection model is not null and {@code this} is selected. */ public final boolean isSelected() { return this.selectionModel != null && this.selectionModel.getSelection() == this; } final void remove() { final JComponent parent = (JComponent) this.getParent(); if (parent != null) { parent.remove(this); // Update scroll pane parent.revalidate(); parent.getRootPane().repaint(); } } private final void setup() { final GridBagConstraints constraints = new GridBagConstraints(); { constraints.gridx = 0; constraints.gridy = 0; constraints.weightx = 0.0; constraints.weighty = 0.0; constraints.insets = new Insets(VERTICAL_INSET, LARGE_HORIZONTAL_INSET, VERTICAL_INSET, 0); constraints.fill = GridBagConstraints.NONE; GUITools.add(this, this.createFileIconLabel(), constraints); } { ++constraints.gridx; constraints.weightx = 1.0; constraints.insets = new Insets(VERTICAL_INSET, LARGE_HORIZONTAL_INSET, VERTICAL_INSET, LARGE_HORIZONTAL_INSET); constraints.fill = GridBagConstraints.HORIZONTAL; GUITools.add(this, this.createProgressBar(), constraints); } { ++constraints.gridx; constraints.weightx = 0.0; constraints.insets = new Insets(VERTICAL_INSET, 0, VERTICAL_INSET, SMALL_HORIZONTAL_INSET); constraints.fill = GridBagConstraints.NONE; GUITools.add(this, this.createStartPauseRetryButton(), constraints); } { ++constraints.gridx; GUITools.add(this, this.createCancelRemoveButton(), constraints); } this.setMaximumSize(new Dimension(Integer.MAX_VALUE, MAXIMUM_HEIGHT)); this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, DEFAULT_BORDER_COLOR)); } /** * * TODO doc * * @return * <br>A non-null value * <br>A new value */ private final JLabel createFileIconLabel() { final JLabel result = new JLabel(getIconForFileName(this.getOperation().getFileName())); resizeIconIfTooSmall(result); result.addMouseListener(new DragMouseHandler()); result.setTransferHandler(new TransferHandler("icon") { @Override protected final void exportDone(final JComponent source, final Transferable data, final int action) { if (TransferHandler.NONE == action) { OperationComponent.this.getOperation().getController().start(); } } @Override public final Icon getVisualRepresentation(final Transferable transferable) { return result.getIcon(); } private static final long serialVersionUID = 9018854963146765450L; }); return result; } /** * TODO doc * * @return * <br>A non-null value * <br>A new value */ private final JProgressBar createProgressBar() { final JProgressBar result = new JProgressBar(); result.setString(this.getOperation().getFileName() + " (queued)"); result.setStringPainted(true); this.getOperation().addOperationListener(new AbstractOperationListener() { @Override protected final void doStateChanged() { super.doStateChanged(); final Operation operation = OperationComponent.this.getOperation(); switch (operation.getState()) { case CANCELED: case DONE: case PAUSED: case PROGRESSING: this.setProgressingString(operation); break; case QUEUED: result.setString(operation.getFileName() + " (queued)"); break; case REMOVED: break; } } @Override protected final void doProgressChanged() { super.doProgressChanged(); final Operation operation = OperationComponent.this.getOperation(); result.setValue(this.getProgressPercentage(operation)); this.setProgressingString(operation); } /** * TODO doc * * @param operation * <br>Should not be null */ private final void setProgressingString(final Operation operation) { result.setString(operation.getFileName() + " (" + this.getProgressPercentage(operation) + "%)"); } /** * TODO doc * * @param operation * <br>Should not be null * @return * <br>Range: {@code [0 .. 100]} */ private final int getProgressPercentage(final Operation operation) { return (int) (operation.getProgress() * 100.0); } }); return result; } /** * * TODO doc * @return * <br>A non-null value * <br>A new value */ private final JButton createCancelRemoveButton() { final JButton result = rollover(new JButton(this.new CancelRemoveAction()), "remove", false); this.getOperation().addOperationListener(new AbstractOperationListener() { @Override protected final void doStateChanged() { super.doStateChanged(); switch (OperationComponent.this.getOperation().getState()) { case PAUSED: case PROGRESSING: rollover(result, "cancel", false); break; case CANCELED: case DONE: case QUEUED: case REMOVED: rollover(result, "remove", false); break; } } }); return result; } /** * TODO doc * * @return * <br>A non-null value * <br>A new value */ private final JButton createStartPauseRetryButton() { final JButton result = rollover(new JButton(this.new StartPauseAction()), "start", false); this.getOperation().addOperationListener(new AbstractOperationListener() { @Override protected final void doStateChanged() { super.doStateChanged(); switch (OperationComponent.this.getOperation().getState()) { case PAUSED: case QUEUED: rollover(result, "start", false); break; case PROGRESSING: rollover(result, "pause", false); break; case CANCELED: rollover(result, "retry", false); break; case DONE: rollover(result, "done", false); break; case REMOVED: break; } } }); return result; } /** * * TODO doc * * @author codistmonk (creation 2010-06-14) * */ private class AbstractOperationListener implements Operation.Listener { /** * Protected default constructor to suppress visibility warnings. */ protected AbstractOperationListener() { // Do nothing } @Override public final void progressChanged() { SwingUtilities.invokeLater(new Runnable() { @Override public final void run() { AbstractOperationListener.this.doProgressChanged(); } }); } @Override public final void stateChanged() { SwingUtilities.invokeLater(new Runnable() { @Override public final void run() { AbstractOperationListener.this.doStateChanged(); } }); } /** * Executed in the AWT Event Dispatching Thread. * * @throws IllegalStateException if the current thread is not the AWT Event Dispatching Thread */ protected void doProgressChanged() { GUITools.checkAWT(); } /** * Executed in the AWT Event Dispatching Thread. * * @throws IllegalStateException if the current thread is not the AWT Event Dispatching Thread */ protected void doStateChanged() { GUITools.checkAWT(); } } /** * * @author codistmonk (creation 2010-05-20) * */ private class StartPauseAction extends AbstractAction { StartPauseAction() { super(""); } @Override public final void actionPerformed(final ActionEvent event) { final Operation operation = OperationComponent.this.getOperation(); final Controller controller = operation.getController(); switch (operation.getState()) { case PAUSED: case QUEUED: case CANCELED: try { controller.start(); } catch (final Throwable exception) { exception.printStackTrace(); } break; case PROGRESSING: controller.pause(); break; case DONE: case REMOVED: break; } } private static final long serialVersionUID = 2945622216824469468L; } /** * * @author codistmonk (creation 2010-05-20) * */ private class CancelRemoveAction extends AbstractAction { /** * Package-private constructor to suppress visibility warnings. */ CancelRemoveAction() { // Do nothing } @Override public final void actionPerformed(final ActionEvent event) { final Operation operation = OperationComponent.this.getOperation(); final Controller controller = operation.getController(); switch (operation.getState()) { case PAUSED: case PROGRESSING: controller.cancel(); break; case CANCELED: case DONE: case QUEUED: case REMOVED: controller.remove(); OperationComponent.this.remove(); break; } } private static final long serialVersionUID = -2137598910170961094L; } /** * * TODO doc * * @author codistmonk (creation 2010-06-25) * */ private final class DragMouseHandler extends MouseAdapter { /** * Package-private default constructor to suppress visibility warnings. */ DragMouseHandler() { // Do nothing } @Override public final void mousePressed(final MouseEvent event) { if (OperationComponent.this.getOperation().getLocalFile() == null) { final JComponent component = (JComponent) event.getComponent(); component.getTransferHandler().exportAsDrag(component, event, TransferHandler.COPY); } } } private static final long serialVersionUID = 195201935191732396L; public static final int MAXIMUM_HEIGHT = 72; public static final int LARGE_HORIZONTAL_INSET = 8; public static final int SMALL_HORIZONTAL_INSET = 4; public static final int VERTICAL_INSET = 16; public static final int PREFERRED_FILE_ICON_SIZE = 32; public static final int FILE_ICON_SCALE = 2; public static final Color DEFAULT_BACKGROUND_COLOR = Color.WHITE; public static final Color ALTERNATE_BACKGROUND_COLOR = Color.LIGHT_GRAY; public static final Color DEFAULT_BORDER_COLOR = Color.BLACK; /** * Creates and returns a temporary file that will be deleted upon exit. * * @param prefix * <br>Should not be null * @param suffix * <br>Should not be null * @return * <br>A possibly null value * <br>A new value */ private static final File createTemporaryFile(final String prefix, final String suffix) { File result = null; try { result = File.createTempFile(prefix, suffix); result.deleteOnExit(); } catch (final IOException exception) { exception.printStackTrace(); } return result; } /** * Returns the default system icon for a file named {@code fileName}. * * @param fileName * <br>Should not be null * @return * <br>A possibly null value */ private static final Icon getIconForFileName(final String fileName) { final File temporaryFile = createTemporaryFile("tmp", fileName); // Try to get a big icon try { Image iconImage = null; { // Try to use sun.awt final Object shellFolder = Class.forName("sun.awt.shell.ShellFolder") .getMethod("getShellFolder", File.class).invoke(null, temporaryFile); iconImage = (Image) shellFolder.getClass().getMethod("getIcon", boolean.class) .invoke(shellFolder, true); } if (iconImage != null) { return new ImageIcon(iconImage); } } catch (final Exception exception) { exception.printStackTrace(); } // As a last resort, use a small icon return new JFileChooser().getIcon(temporaryFile); } /** * * TODO doc * * @param iconLabel * <br>Should not be null * <br>Input-output parameter */ private static final void resizeIconIfTooSmall(final JLabel iconLabel) { final Icon icon = iconLabel.getIcon(); if (icon.getIconWidth() < PREFERRED_FILE_ICON_SIZE) { final BufferedImage scaledImage = new BufferedImage(icon.getIconWidth() * FILE_ICON_SCALE, icon.getIconHeight() * FILE_ICON_SCALE, BufferedImage.TYPE_INT_ARGB); final Graphics2D graphics = scaledImage.createGraphics(); graphics.scale(FILE_ICON_SCALE, FILE_ICON_SCALE); graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); icon.paintIcon(iconLabel, graphics, 0, 0); iconLabel.setIcon(new ImageIcon(scaledImage)); } } /** * * @author codistmonk (creation 2010-05-23) * */ public static class SelectionModel { private OperationComponent selection; /** * * @return * <br>A possibly null value * <br>A shared value */ public final OperationComponent getSelection() { return this.selection; } /** * * @param selection * <br>Can be null * <br>Shared parameter */ public final void setSelection(final OperationComponent selection) { this.selection = selection; } } }