JAL-3878 Introduce IProgressIndicator#addProgressBar
[jalview.git] / src / jalview / gui / ProgressBar.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.util.MessageManager;
24
25 import java.awt.BorderLayout;
26 import java.awt.Component;
27 import java.awt.GridLayout;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.util.Hashtable;
31 import java.util.Map;
32
33 import javax.swing.JButton;
34 import javax.swing.JLabel;
35 import javax.swing.JPanel;
36 import javax.swing.JProgressBar;
37 import javax.swing.SwingUtilities;
38
39 /**
40  * A class to manage multiple progress bars embedded in a JPanel.
41  */
42 /*
43  * Refactored from code duplicated in AlignFrame, PCAPanel, WebserviceInfo.
44  */
45 public class ProgressBar implements IProgressIndicator
46 {
47   /*
48    * Progress bars in progress, keyed by any arbitrary long value
49    */
50   Map<Long, JPanel> progressBars;
51
52   /*
53    * Optional handlers for the progress bars
54    */
55   Map<Long, IProgressIndicatorHandler> progressBarHandlers;
56
57   /*
58    * The panel containing the progress bars - must have GridLayout
59    */
60   private JPanel statusPanel;
61
62   /*
63    * Optional label where a status update message can be written on completion
64    * of progress
65    */
66   private JLabel statusBar;
67
68   /**
69    * Constructor. Note that the container of the progress bars, and the status
70    * bar to which an optional completion message is written, should be unchanged
71    * for the lifetime of this object for consistent behaviour.
72    * 
73    * @param container
74    *          the panel holding the progress bars; must have GridLayout manager
75    * @param statusBar
76    *          an optional place to write a message when a progress bar is
77    *          removed
78    */
79   public ProgressBar(JPanel container, JLabel statusBar)
80   {
81     if (container == null)
82     {
83       throw new NullPointerException();
84     }
85     if (!GridLayout.class
86         .isAssignableFrom(container.getLayout().getClass()))
87     {
88       throw new IllegalArgumentException("Container must have GridLayout");
89     }
90     this.statusPanel = container;
91     this.statusBar = statusBar;
92     this.progressBars = new Hashtable<>();
93     this.progressBarHandlers = new Hashtable<>();
94
95   }
96
97   /**
98    * Returns true if any progress bars are still active
99    * 
100    * @return
101    */
102   @Override
103   public boolean operationInProgress()
104   {
105     if (progressBars != null && progressBars.size() > 0)
106     {
107       return true;
108     }
109     return false;
110   }
111
112   /**
113    * First call for a given id will show the message as a new progress bar. A
114    * second call with the same id will remove it. The 'removal' message is
115    * written to the status bar field (if neither is null).
116    * 
117    * To avoid progress bars being left orphaned, ensure their removal is
118    * performed in a finally block if there is any risk of an error during
119    * execution.
120    */
121   @Override
122   public void setProgressBar(final String message, final long id)
123   {
124     SwingUtilities.invokeLater(new Runnable()
125     {
126       @Override
127       public void run()
128       {
129         if (progressBars.containsKey(id))
130         {
131           /*
132            * Progress bar is displayed for this id - remove it now, and any handler
133            */
134           removeProgressBar(id);
135           if (message != null && statusBar != null)
136           {
137             statusBar.setText(message);
138           }
139         }
140         else
141         {
142           /*
143            * No progress bar for this id - add one now
144            */
145           addProgressBar(id, message);
146         }
147       }
148     });
149
150   }
151
152   /**
153    * Add a progress bar for the given id if it doesn't exist displaying the
154    * provided message. Subsequent calls do nothing.
155    * 
156    * @param id
157    *          progress bar identifier
158    * @param message
159    *          displayed message
160    */
161   @Override
162   public void addProgressBar(final long id, final String message)
163   {
164     if (progressBars.containsKey(id))
165       return;
166     JPanel progressPanel = new JPanel(new BorderLayout(10, 5));
167     progressBars.put(id, progressPanel);
168     Runnable r = () -> {
169       JProgressBar progressBar = new JProgressBar();
170       progressBar.setIndeterminate(true);
171       progressPanel.add(new JLabel(message), BorderLayout.WEST);
172       progressPanel.add(progressBar, BorderLayout.CENTER);
173       addRow(progressPanel);
174       refreshLayout();
175     };
176     if (SwingUtilities.isEventDispatchThread())
177       r.run();
178     else
179       SwingUtilities.invokeLater(r);
180   }
181
182   /**
183    * Remove a progress bar for the given id if it exists. Subsequent calls do
184    * nothing.
185    * 
186    * @param id
187    *          id of the progress bar to be removed
188    */
189   @Override
190   public void removeProgressBar(final long id)
191   {
192     JPanel progressPanel = progressBars.remove(id);
193     if (progressPanel == null)
194       return;
195     progressBarHandlers.remove(id);
196     Runnable r = () -> {
197       removeRow(progressPanel);
198       refreshLayout();
199     };
200     if (SwingUtilities.isEventDispatchThread())
201       r.run();
202     else
203       SwingUtilities.invokeLater(r);
204   }
205
206   /**
207    * Lays out progress bar container hierarchy
208    */
209   protected void refreshLayout()
210   {
211     /*
212      * lay out progress bar container hierarchy
213      */
214     Component root = SwingUtilities.getRoot(statusPanel);
215     if (root != null)
216     {
217       root.validate();
218     }
219   }
220
221   /**
222    * Remove one row with a progress bar, in a thread-safe manner
223    * 
224    * @param progressPanel
225    */
226   protected void removeRow(JPanel progressPanel)
227   {
228     synchronized (statusPanel)
229     {
230       statusPanel.remove(progressPanel);
231       GridLayout layout = (GridLayout) statusPanel.getLayout();
232       layout.setRows(layout.getRows() - 1);
233       statusPanel.remove(progressPanel);
234     }
235   }
236
237   /**
238    * Add one row with a progress bar, in a thread-safe manner
239    * 
240    * @param progressPanel
241    */
242   protected void addRow(JPanel progressPanel)
243   {
244     synchronized (statusPanel)
245     {
246       GridLayout layout = (GridLayout) statusPanel.getLayout();
247       layout.setRows(layout.getRows() + 1);
248       statusPanel.add(progressPanel);
249     }
250   }
251
252   /**
253    * Add a 'Cancel' handler for the given progress bar id. This should be called
254    * _after_ setProgressBar to have any effect.
255    */
256   @Override
257   public void registerHandler(final long id,
258       final IProgressIndicatorHandler handler)
259   {
260     final IProgressIndicator us = this;
261
262     SwingUtilities.invokeLater(new Runnable()
263     {
264       @Override
265       public void run()
266       {
267         final JPanel progressPanel = progressBars.get(id);
268         if (progressPanel == null)
269         {
270           System.err.println(
271               "call setProgressBar before registering the progress bar's handler.");
272           return;
273         }
274
275         /*
276          * Nothing useful to do if not a Cancel handler
277          */
278         if (!handler.canCancel())
279         {
280           return;
281         }
282
283         progressBarHandlers.put(id, handler);
284         JButton cancel = new JButton(
285             MessageManager.getString("action.cancel"));
286         cancel.addActionListener(new ActionListener()
287         {
288
289           @Override
290           public void actionPerformed(ActionEvent e)
291           {
292             handler.cancelActivity(id);
293             us.setProgressBar(MessageManager
294                 .formatMessage("label.cancelled_params", new Object[]
295                 { ((JLabel) progressPanel.getComponent(0)).getText() }),
296                 id);
297           }
298         });
299         progressPanel.add(cancel, BorderLayout.EAST);
300         refreshLayout();
301
302       }
303     });
304   }
305
306 }