/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.dialog.file; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.WindowEvent; import java.util.Vector; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.conf.MuConfigurations; import com.mucommander.conf.MuPreference; import com.mucommander.conf.MuPreferences; import com.mucommander.job.FileJob; import com.mucommander.job.FileJobListener; import com.mucommander.job.FileJobState; import com.mucommander.job.JobProgress; import com.mucommander.job.JobListener; import com.mucommander.job.JobsManager; import com.mucommander.job.impl.TransferFileJob; import com.mucommander.text.DurationFormat; import com.mucommander.text.SizeFormat; import com.mucommander.text.Translator; import com.mucommander.ui.button.ButtonChoicePanel; import com.mucommander.ui.button.CollapseExpandButton; import com.mucommander.ui.chooser.SizeChooser; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.StatusBar; /** * This dialog informs the user of the progress made by a FileJob and allows to control it: pause/resume it, stop it, * limit transfer rate... * * @author Maxence Bernard */ public class ProgressDialog extends FocusDialog implements ActionListener, ItemListener, ChangeListener, FileJobListener, JobListener { private static final Logger LOGGER = LoggerFactory.getLogger(ProgressDialog.class); private JLabel currentFileLabel; private JLabel totalTransferredLabel; private JProgressBar totalProgressBar; private JProgressBar currentFileProgressBar; private JLabel currentSpeedLabel; private JCheckBox limitSpeedCheckBox; private SizeChooser speedChooser; private JLabel elapsedTimeLabel; private SpeedGraph speedGraph; private CollapseExpandButton collapseExpandButton; private ButtonChoicePanel buttonsChoicePanel; private JButton pauseResumeButton; private JButton skipButton; private JButton stopButton; private JCheckBox closeWhenFinishedCheckBox; private JButton hideButton; private FileJob job; private TransferFileJob transferFileJob; private boolean firstTimeActivated = true; // Button icons private final static String RESUME_ICON = "resume.png"; private final static String PAUSE_ICON = "pause.png"; private final static String SKIP_ICON = "skip.png"; private final static String STOP_ICON = "stop.png"; private final static String HIDE_ICON = "hide.png"; private final static String CURRENT_SPEED_ICON = "speed.png"; // Dialog width is constrained to 410, height is not an issue (always the same) private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(410,10000); private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(410,0); /** Height allocated to the 'speed graph' */ private final static int SPEED_GRAPH_HEIGHT = 80; static { // Disable JProgressBar animation which is a real CPU hog under Mac OS X UIManager.put("ProgressBar.repaintInterval", Integer.MAX_VALUE); } public ProgressDialog(MainFrame mainFrame, String title) { super(mainFrame, title, mainFrame); // Sets maximum and minimum dimensions for this dialog setMaximumSize(MAXIMUM_DIALOG_DIMENSION); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setResizable(false); } private void initUI() { Container contentPane = getContentPane(); totalProgressBar = new JProgressBar(); totalProgressBar.setStringPainted(true); totalProgressBar.setAlignmentX(LEFT_ALIGNMENT); currentFileLabel = new JLabel(job.getStatusString()); currentFileLabel.setAlignmentX(LEFT_ALIGNMENT); YBoxPanel yPanel = new YBoxPanel(); // 2 progress bars if (transferFileJob !=null) { yPanel.add(currentFileLabel); currentFileProgressBar = new JProgressBar(); currentFileProgressBar.setStringPainted(true); yPanel.add(currentFileProgressBar); yPanel.addSpace(10); totalTransferredLabel = new JLabel(Translator.get("progress_dialog.starting")); yPanel.add(totalTransferredLabel); yPanel.add(totalProgressBar); } // Single progress bar else { yPanel.add(currentFileLabel); yPanel.add(totalProgressBar); } yPanel.addSpace(10); elapsedTimeLabel = new JLabel(Translator.get("progress_dialog.elapsed_time")+": "); elapsedTimeLabel.setIcon(IconManager.getIcon(IconManager.STATUS_BAR_ICON_SET, StatusBar.WAITING_ICON)); yPanel.add(elapsedTimeLabel); if(transferFileJob!=null) { JPanel tempPanel = new JPanel(new BorderLayout()); this.currentSpeedLabel = new JLabel(); updateCurrentSpeedLabel(""); currentSpeedLabel.setIcon(IconManager.getIcon(IconManager.PROGRESS_ICON_SET, CURRENT_SPEED_ICON)); tempPanel.add(currentSpeedLabel, BorderLayout.WEST); YBoxPanel advancedPanel = new YBoxPanel(); this.speedGraph = new SpeedGraph(); speedGraph.setPreferredSize(new Dimension(0, SPEED_GRAPH_HEIGHT)); advancedPanel.add(speedGraph); advancedPanel.addSpace(5); JPanel tempPanel2 = new JPanel(new BorderLayout()); this.limitSpeedCheckBox = new JCheckBox(Translator.get("progress_dialog.limit_speed")+":", false); limitSpeedCheckBox.addItemListener(this); tempPanel2.add(limitSpeedCheckBox, BorderLayout.WEST); speedChooser = new SizeChooser(true); speedChooser.setEnabled(false); speedChooser.addChangeListener(this); tempPanel2.add(speedChooser, BorderLayout.EAST); advancedPanel.add(tempPanel2); advancedPanel.addSpace(5); this.collapseExpandButton = new CollapseExpandButton(Translator.get("progress_dialog.advanced"), advancedPanel, true); collapseExpandButton.setExpandedState(MuConfigurations.getPreferences().getVariable(MuPreference.PROGRESS_DIALOG_EXPANDED, MuPreferences.DEFAULT_PROGRESS_DIALOG_EXPANDED)); tempPanel.add(collapseExpandButton, BorderLayout.EAST); yPanel.add(tempPanel); yPanel.addSpace(5); yPanel.add(advancedPanel); } closeWhenFinishedCheckBox = new JCheckBox(Translator.get("progress_dialog.close_when_finished")); closeWhenFinishedCheckBox.setSelected(MuConfigurations.getPreferences().getVariable(MuPreference.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED, MuPreferences.DEFAULT_PROGRESS_DIALOG_CLOSE_WHEN_FINISHED)); yPanel.add(closeWhenFinishedCheckBox); yPanel.add(Box.createVerticalGlue()); contentPane.add(yPanel, BorderLayout.CENTER); pauseResumeButton = new JButton(Translator.get("pause"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, PAUSE_ICON)); pauseResumeButton.addActionListener(this); if(transferFileJob!=null) { skipButton = new JButton(Translator.get("skip"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, SKIP_ICON)); skipButton.addActionListener(this); } stopButton = new JButton(Translator.get("stop"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, STOP_ICON)); stopButton.addActionListener(this); hideButton = new JButton(Translator.get("progress_dialog.hide"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, HIDE_ICON)); hideButton.addActionListener(this); this.buttonsChoicePanel = new ButtonChoicePanel( skipButton==null ? new JButton[] {pauseResumeButton, stopButton, hideButton} : new JButton[] {pauseResumeButton, skipButton, stopButton, hideButton}, 0, getRootPane()); contentPane.add(buttonsChoicePanel, BorderLayout.SOUTH); // Cancel button receives initial focus setInitialFocusComponent(stopButton); // Enter triggers cancel button getRootPane().setDefaultButton(stopButton); } public void start(FileJob job) { this.job = job; // Listen to job state changes job.addFileJobListener(this); if (job instanceof TransferFileJob) this.transferFileJob = (TransferFileJob)job; initUI(); JobsManager.getInstance().addJob(job); JobsManager.getInstance().addJobListener(this); if (job.isRunInBackground()) { firstTimeActivated = false; job.start(); } else showDialog(); } /** * Stops repaint thread. */ public void stop() { JobsManager.getInstance().removeJobListener(this); } // /** // * This method is called by the registered FileJob starts each time a new file is being processed. // */ // public void notifyCurrentFileChanged() { // // Update current file label // currentFileLabel.setText(job.getStatusString()); // } private void updateThroughputLimit() { transferFileJob.setThroughputLimit(limitSpeedCheckBox.isSelected()?speedChooser.getValue():-1); } private void updateCurrentSpeedLabel(String value) { currentSpeedLabel.setText(Translator.get("progress_dialog.current_speed")+": "+value); } //////////////////////////////////// // FileJobListener implementation // //////////////////////////////////// @Override public void jobStateChanged(FileJob source, FileJobState oldState, FileJobState newState) { LOGGER.debug("currentThread="+Thread.currentThread()+" oldState="+oldState+" newState="+newState); switch (newState) { case INTERRUPTED: // Stop repaint thread and dispose dialog stop(); dispose(); break; case FINISHED: // Dispose dialog only if 'Close when finished option' is selected if(closeWhenFinishedCheckBox.isSelected()) { // Stop repaint thread and dispose dialog stop(); dispose(); // If not, disable components to indicate that the job is finished and leave the dialog open } else { // Repaint thread should not be stopped now, it will die naturally after updating labels and progress // bars to indicate that the job is finished // Change 'Stop' button's label to 'Close' stopButton.setText(Translator.get("close")); // Disable components pauseResumeButton.setEnabled(false); if(transferFileJob!=null) { skipButton.setEnabled(false); limitSpeedCheckBox.setEnabled(false); speedChooser.setEnabled(false); } } break; case PAUSED: pauseResumeButton.setText(Translator.get("resume")); pauseResumeButton.setIcon(IconManager.getIcon(IconManager.PROGRESS_ICON_SET, RESUME_ICON)); // Update buttons mnemonics buttonsChoicePanel.updateMnemonics(); if(transferFileJob!=null) updateCurrentSpeedLabel("N/A"); break; case RUNNING: pauseResumeButton.setText(Translator.get("pause")); pauseResumeButton.setIcon(IconManager.getIcon(IconManager.PROGRESS_ICON_SET, PAUSE_ICON)); // Update buttons mnemonics buttonsChoicePanel.updateMnemonics(); break; default: } } // Refresh current file label in a separate thread, more frequently than other components to give a sense // of speed when small files are being transferred. // This 'pull' approach allows to throttle the number label updates which have a cost VS updating the label // for each file being processed (job notifications) which can hog the CPU when lots of small files // are being transferred. private void updateProgressLabel(JobProgress progress) { currentFileLabel.setText(progress.getJobStatusString()); } private void updateProgressUI(JobProgress progress) { if (progress.isTransferFileJob()) { currentFileProgressBar.setValue(progress.getFilePercentInt()); currentFileProgressBar.setString(progress.getFileProgressText()); // Update total transferred label totalTransferredLabel.setText( Translator.get("progress_dialog.transferred", SizeFormat.format(progress.getBytesTotal(), SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_LONG| SizeFormat.ROUND_TO_KB), SizeFormat.format(progress.getTotalBps(), SizeFormat.UNIT_SPEED| SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_SHORT| SizeFormat.ROUND_TO_KB)) ); // Add new immediate bytes per second speed sample to speed graph and label and repaint it // Skip this sample if job was paused and resumed, speed would not be accurate if (progress.getLastTime()>progress.getJobPauseStartDate()) { speedGraph.addSample(progress.getCurrentBps()); updateCurrentSpeedLabel(SizeFormat.format(progress.getCurrentBps(), SizeFormat.UNIT_SPEED| SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_SHORT)); } } totalProgressBar.setValue(progress.getTotalPercentInt()); totalProgressBar.setString(progress.getTotalProgressText()); // Update elapsed time label elapsedTimeLabel.setText(Translator.get("progress_dialog.elapsed_time")+": "+DurationFormat.format(progress.getEffectiveJobTime())); } ///////////////////////////// // JobProgress listener // ///////////////////////////// @Override public void jobProgress(FileJob source, boolean fullUpdate) { if (job.equals(source)) { updateProgressLabel(source.getJobProgress()); if (fullUpdate) { updateProgressUI(source.getJobProgress()); } } } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source==stopButton) { FileJobState jobState = job.getState(); // Case when 'Close when finished' isn't selected, stop button's action becomes 'Close' if(jobState == FileJobState.FINISHED || jobState == FileJobState.INTERRUPTED) dispose(); else job.interrupt(); } else if(source==skipButton) { transferFileJob.skipCurrentFile(); } else if(source==pauseResumeButton) { // Pause/resume job job.setPaused(job.getState() != FileJobState.PAUSED); } else if(source==hideButton) { job.setRunInBackground(true); setVisible(false); } } ///////////////////////////////// // ItemListener implementation // ///////////////////////////////// public void itemStateChanged(ItemEvent e) { Object source = e.getSource(); if(source==limitSpeedCheckBox) { boolean isEnabled = limitSpeedCheckBox.isSelected(); speedChooser.setEnabled(isEnabled); updateThroughputLimit(); } } /////////////////////////////////// // ChangeListener implementation // /////////////////////////////////// public void stateChanged(ChangeEvent e) { if(e.getSource()==speedChooser) { updateThroughputLimit(); } } /////////////////////////////////////// // Overridden WindowListener methods // /////////////////////////////////////// @Override public void windowActivated(WindowEvent e) { // This method is called each time the dialog is activated super.windowActivated(e); if(firstTimeActivated) { firstTimeActivated = false; this.job.start(); } } @Override public void windowClosed(WindowEvent e) { super.windowClosed(e); // Stop repaint thread if it isn't already stop(); // Stop job if it isn't already FileJobState jobState = job.getState(); if (jobState != FileJobState.FINISHED && jobState != FileJobState.INTERRUPTED) job.interrupt(); // Remember 'advanced panel' expanded state if (collapseExpandButton != null) MuConfigurations.getPreferences().setVariable(MuPreference.PROGRESS_DIALOG_EXPANDED, collapseExpandButton.getExpandedState()); // Remember 'close window when finished' option state MuConfigurations.getPreferences().setVariable(MuPreference.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED, closeWhenFinishedCheckBox.isSelected()); } /** * Transfer speed graph. */ private class SpeedGraph extends JPanel { private final Color GRAPH_OUTLINE_COLOR = new Color(70, 70, 70); private final Color GRAPH_FILL_COLOR = new Color(215, 215, 215); private final Color BPS_LIMIT_COLOR = new Color(204, 0, 0); private static final int LINE_SPACING = 6; private static final int NB_SAMPLES_MAX = 320; private static final int STROKE_WIDTH = 1; private java.util.List<Long> samples = new Vector<Long>(NB_SAMPLES_MAX); private Stroke lineStroke = new BasicStroke(STROKE_WIDTH); private SpeedGraph() { } private void addSample(long bytesPerSecond) { synchronized(samples) { // Ensures that paint() is not currently accessing the Vector // Capacity reached, remove first sample if(samples.size()==NB_SAMPLES_MAX) samples.remove(0); // Add sample to the vector samples.add(bytesPerSecond); } repaint(); } @Override public void paint(Graphics g) { Graphics2D g2d = (Graphics2D)g; // Enable antialiasing, looks way better g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int width = getWidth(); int height = getHeight(); // Fill the background with the panel's background color g.setColor(getBackground()); g.fillRect(0, 0, width, height); g2d.setStroke(lineStroke); synchronized(samples) { // Ensures that addSample() is not currently accessing the Vector // Number of collected sample int nbSamples = samples.size(); // Number of displayable samples based on their spacing int nbDisplayableSamples = (width-2*STROKE_WIDTH)/LINE_SPACING; // Index of the first sample int firstSample = nbSamples>nbDisplayableSamples?nbSamples-nbDisplayableSamples:0; // Number of lines to be drawn int nbLines = Math.min(nbSamples, nbDisplayableSamples); // Calculate the maximum bytes per second of all the samples to be displayed long maxBps = 0; for(int i=firstSample; i<firstSample+nbLines; i++) { long sample = samples.get(i); if(sample>maxBps) maxBps = sample; } // Y-scale projection ratio, leave some space on both sides of the graph float yRatio = maxBps/((float)height-2*STROKE_WIDTH); // Draw throughput limit as an horizontal line, only if there is a limit long bpsLimit = transferFileJob.getThroughputLimit(); if(bpsLimit>0) { g.setColor(BPS_LIMIT_COLOR); int y = height-STROKE_WIDTH-(int)(bpsLimit/yRatio); g.drawLine(0, y, width, y); } // Fill the graph g.setColor(GRAPH_FILL_COLOR); int x = STROKE_WIDTH; Polygon p = new Polygon(); int sampleOffset = firstSample; for(int l=0; l<nbLines; l++) { p.addPoint(x, height-STROKE_WIDTH-(int)((Long) samples.get(sampleOffset++) /yRatio)); x+=LINE_SPACING; } p.addPoint(x-LINE_SPACING, height-1); p.addPoint(0, height-1); g.fillPolygon(p); // Draw the graph outline in a darker color g.setColor(GRAPH_OUTLINE_COLOR); x = STROKE_WIDTH; sampleOffset = firstSample; for(int l=0; l<nbLines-1; l++) { g.drawLine(x, height-STROKE_WIDTH-(int)((Long) samples.get(sampleOffset) /yRatio), (x+=LINE_SPACING), height-STROKE_WIDTH-(int)((Long) samples.get(++sampleOffset) /yRatio)); } // Draw an horizontal line at the bottom of the graph g.drawLine(0, height-1, width-1, height-1); // Unsuccessful rendering test using curves // GeneralPath gp = new GeneralPath(); // int x = STROKE_WIDTH; // gp.moveTo(x, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset++)).longValue()/yRatio)); // x += LINE_SPACING; // for(int l=1; l<nbLines-2; l+=2) { // gp.quadTo( // x, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset)).longValue()/yRatio), // x+LINE_SPACING, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset+1)).longValue()/yRatio) // ); // // sampleOffset += 2; // x += 2*LINE_SPACING; // // g2d.draw(gp); // } } } } }