001    /*
002    Copyright (c) 1996-2011, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.clApp.uploader;
031    
032    import java.awt.BorderLayout;
033    import java.awt.Color;
034    import java.awt.Component;
035    import java.awt.Dimension;
036    import java.awt.GridBagConstraints;
037    import java.awt.GridBagLayout;
038    import java.awt.Insets;
039    import java.awt.event.ActionEvent;
040    import java.awt.event.ActionListener;
041    import java.awt.event.KeyEvent;
042    import java.awt.event.MouseAdapter;
043    import java.awt.event.MouseEvent;
044    import java.awt.event.WindowAdapter;
045    import java.awt.event.WindowEvent;
046    import java.io.File;
047    import java.io.IOException;
048    import java.util.ArrayList;
049    import java.util.Arrays;
050    import java.util.Collections;
051    import java.util.Date;
052    import java.util.Enumeration;
053    import java.util.List;
054    import java.util.Map;
055    import java.util.SortedSet;
056    import java.util.Vector;
057    import java.util.concurrent.atomic.AtomicInteger;
058    
059    import javax.jnlp.BasicService;
060    import javax.jnlp.FileContents;
061    import javax.jnlp.SingleInstanceListener;
062    import javax.swing.AbstractButton;
063    import javax.swing.Action;
064    import javax.swing.BorderFactory;
065    import javax.swing.DefaultComboBoxModel;
066    import javax.swing.ImageIcon;
067    import javax.swing.JButton;
068    import javax.swing.JCheckBox;
069    import javax.swing.JComboBox;
070    import javax.swing.JComponent;
071    import javax.swing.JEditorPane;
072    import javax.swing.JFileChooser;
073    import javax.swing.JFrame;
074    import javax.swing.JLabel;
075    import javax.swing.JList;
076    import javax.swing.JMenu;
077    import javax.swing.JMenuBar;
078    import javax.swing.JOptionPane;
079    import javax.swing.JPanel;
080    import javax.swing.JPasswordField;
081    import javax.swing.JProgressBar;
082    import javax.swing.JScrollPane;
083    import javax.swing.JSpinner;
084    import javax.swing.JTabbedPane;
085    import javax.swing.JTable;
086    import javax.swing.JTextArea;
087    import javax.swing.JTextField;
088    import javax.swing.ListCellRenderer;
089    import javax.swing.ListSelectionModel;
090    import javax.swing.SpinnerModel;
091    import javax.swing.SpinnerNumberModel;
092    import javax.swing.Timer;
093    import javax.swing.TransferHandler;
094    import javax.swing.UIManager;
095    import javax.swing.event.ListSelectionEvent;
096    import javax.swing.event.ListSelectionListener;
097    import javax.swing.filechooser.FileFilter;
098    import javax.swing.table.AbstractTableModel;
099    
100    import org.hd.d.pg2k.svrCore.AllExhibitImmutableData;
101    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
102    import org.hd.d.pg2k.svrCore.CoreConsts;
103    import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
104    import org.hd.d.pg2k.svrCore.ExhibitName;
105    import org.hd.d.pg2k.svrCore.GenUtils;
106    import org.hd.d.pg2k.svrCore.LocaleBeanBase;
107    import org.hd.d.pg2k.svrCore.Name;
108    import org.hd.d.pg2k.svrCore.Rnd;
109    import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
110    import org.hd.d.pg2k.svrCore.TextUtils;
111    import org.hd.d.pg2k.svrCore.Tuple;
112    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
113    import org.hd.d.pg2k.svrCore.props.GenProps;
114    import org.hd.d.pg2k.svrCore.uploader.ExhibitHandlerBeanBase;
115    import org.hd.d.pg2k.svrCore.uploader.UploadInfoBean;
116    
117    
118    /**Main (UI) class of JWS-based exhibit uploader.
119     * Runs as a Swing App using JNLP resource-access facilities
120     * such as persistence and file read/write.
121     * <p>
122     * This class/file contains as little non-UI code as is reasonably practical,
123     * so that if we change the UI details then other classes should be unaffected.
124     * <p>
125     * Derived initially from Sun's "WebPad" sample JWS/JNLP code.
126     * <p>
127     * We don't really want/need this class to be Serializable,
128     * but this class inherits Serializable from JFrame.
129     */
130    public final class UploaderMain extends JFrame implements ActionListener
131        {
132        /**Central logger instance for uploader; never null.
133         * This instance may log to the status bar and elsewhere.
134         */
135        private final SimpleLoggerIF logger = new SimpleLoggerIF()
136            {
137            public final void log(final String message)
138                {
139                // Log to the Java console.
140                System.out.println(message);
141    
142                // Schedule a job for the event-dispatching thread:
143                // logging this message in the status bar.
144                // We hope that these events do not get re-ordered too often.
145                javax.swing.SwingUtilities.invokeLater(new Runnable(){
146                    public final void run()
147                        { status.setText(message); }
148                    });
149                }
150            };
151    
152        /**If true, only allow upload in a JWS environment. */
153        static final boolean JWS_UPLOAD_ONLY = false;
154    
155        /**Our companion "business-logic" class; never null. */
156        private final UploaderLogic logic = new UploaderLogic(logger);
157    
158        /**The action performed by the "Select" button. */
159        private final SelectAction selectAction = new SelectAction();
160        /**The action performed by the "About" menu entry. */
161        private final AboutAction aboutAction = new AboutAction();
162        /**The action performed by the "Exit" menu entry. */
163        private final ExitAction exitAction = new ExitAction();
164    
165    //    /**Thread-safe List of all the actions; never null. */
166    //    private final java.util.List<JLFAbstractAction> actions = new Vector<JLFAbstractAction>();
167    
168        /**Select our locale; never null. */
169        private final LocaleBeanBase lbb = new LocaleBeanBase();
170    
171        /**Status bar; never null. */
172        private final JLabel status = createStatusBar();
173    
174        /**Single listener instance. */
175        private final SISListener sisListener = new SISListener();
176    
177        /**Bean to hold our current view of the exhibit we are naming; never null. */
178        final UploadInfoBean uib = new UploadInfoBean();
179    
180        /**If nothing else resets it, start with a more friendly number-in-series of 1 rather than 0. */
181        { uib.setNumber(1); }
182    
183        /**Contains user-selected auto/manual file-type extraction; never null. */
184        private final JCheckBox autoSuffixCheckBox = new JCheckBox();
185    
186        /**Handles Mouse over messages on toolbar buttons and menu items; never null. */
187        private final MouseHandler mouseHandler = new MouseHandler(status);
188    
189        /**Background colour we set for read-only informational areas.
190         * We use this to avoid confusion with data-entry areas.
191         * <p>
192         * We may co-ordinate this with the Web site uploader colours.
193         */
194        private static final Color BG_RO_COL = Color.YELLOW;
195    
196    
197        /**Row marked for editing in uploadTable; -1 if none; never null.
198         * Should be ignored if out of range for the current table content.
199         * <p>
200         * We use AtomicInteger for lockless thread-safety.
201         */
202        private final AtomicInteger uploadTableEditRow = new AtomicInteger(-1);
203    
204        ;
205    
206    
207        /**The table model instance for the files selected for upload; never null.
208         * This doesn't hold any data itself,
209         * simply interfaces to the data in logic.selectedFiles.
210         * <p>
211         * Call fireTableDataChanged() or something more fine-grained
212         * when the data has changed to allow proper redrawing.
213         */
214        private final AbstractTableModel uploadTableModel =
215            new UploadFileTableModel(logic, logic.selectedFiles, uploadTableEditRow);
216    
217        /**The table model for the files queued for upload; never null.
218         * This doesn't hold any data itself,
219         * simply interfaces to the data in logic.uploadingFiles.
220         * <p>
221         * Call fireTableDataChanged() or something more fine-grained
222         * when the data has changed to allow proper redrawing.
223         */
224        private final AbstractTableModel queueTableModel =
225            new UploadFileTableModel(logic, logic.uploadingFiles, null);
226    
227        /**Title shown for application. */
228        private static final String APPLICATION_WINDOW_TITLE = "Gallery Uploader";
229    
230        /**Idle time in text entry box before we take opportunity to show updates, ms; strictly positive. */
231        private static final int TEXT_ENTRY_TIMEOUT_MS = 6001;
232    
233    
234        /**Create an instance of the Uploader app main window.
235         * Designed to be called by main().
236         */
237        private UploaderMain()
238            {
239            super(APPLICATION_WINDOW_TITLE);
240    
241            // Set up user actions.
242            initActions();
243    
244            // ASAP, try to avoid multiple instances being started.
245            if(logic.sis != null)
246                { logic.sis.addSingleInstanceListener(sisListener); }
247    
248            // Create the contents of this frame...
249            setJMenuBar(createMenu());
250            final Tuple.Pair<JComponent, ActionListener> mainPane = createMainPane();
251            pollAL = mainPane.second;
252            assert(mainPane != null);
253            assert(mainPane.first != null);
254            assert(mainPane.second != null);
255            getContentPane().add(mainPane.first, BorderLayout.CENTER);
256            getContentPane().add(status, BorderLayout.SOUTH);
257    
258            // Arrange to exit the VM if the window is closed...
259            addWindowListener(new WindowAdapter()
260                {
261                @Override
262                public final void windowClosing(final WindowEvent evt)
263                    {
264                    // Shut down gracefully (and exit()) if allowed to,
265                    // else attempt to veto with an exception.
266                    try { shutdown(); }
267                    catch(final UnsupportedOperationException e) { }
268                    }
269                });
270            }
271    
272        /**Perform any activity required to shut down cleanly, eg save state, then exit.
273         * This should try to avoid taking a long time.
274         * <p>
275         * This may throw an UnsupportedOperationException to try to veto
276         * an exit that the user changes their mind about,
277         * eg because we have work in progress!
278         *
279         * @throws UnsupportedOperationException  if the user vetoes the shut-down
280         */
281        private void shutdown()
282            throws UnsupportedOperationException
283            {
284            // If there is any work in progress
285            // then double-check before allowing shutdown.
286            if((logic.selectedFiles.size() > 0) ||
287               (logic.uploadingFiles.size() > 0))
288                {
289                if(JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(this, "Sure you want to discard current selected files and uploads?", "Quit with work in progess...?", JOptionPane.OK_CANCEL_OPTION))
290                    {
291                    logger.log("Shutdown vetoed...");
292                    throw new UnsupportedOperationException("User vetoed shutdown...");
293                    }
294                }
295    
296            // Do any shutdown required by the non-GUI components...
297            logic.shutdown();
298    
299            // Now allow other instances to be started...
300            if(logic.sis != null)
301                { logic.sis.removeSingleInstanceListener(sisListener); }
302    
303            // Now allow a gracefull exit!
304            System.exit(0);
305            }
306    
307        /**This method should be called before creating the UI to create all the Actions. */
308        private void initActions()
309            {
310    //        actions.clear();
311            registerAction(selectAction);
312            registerAction(aboutAction);
313            registerAction(exitAction);
314            }
315    
316        private void registerAction(final JLFAbstractAction action)
317            {
318            action.addActionListener(this);
319    //        actions.addElement(action);
320            }
321    
322        /**Create the application menu bar. */
323        private JMenuBar createMenu()
324            {
325            final JMenuBar menuBar = new JMenuBar();
326    
327            // Build the File menu
328            final JMenu fileMenu = new JMenu("File");
329            fileMenu.setMnemonic('F');
330            fileMenu.add(exitAction).addMouseListener(mouseHandler);
331    //        fileMenu.setMnemonic('U');
332    //        fileMenu.add(selectAction).addMouseListener(mouseHandler);
333    
334            // Build the help menu
335            final JMenu helpMenu = new JMenu("Help");
336            helpMenu.setMnemonic('H');
337            helpMenu.add(aboutAction).addMouseListener(mouseHandler);
338    
339            menuBar.add(fileMenu);
340            menuBar.add(helpMenu);
341    
342            return menuBar;
343            }
344    
345        /**Create the (main) tabbed pane component and a lister for UI polling. */
346        private Tuple.Pair<JComponent, ActionListener> createMainPane()
347            {
348            final JTabbedPane tabbedPane = new JTabbedPane();
349            final ImageIcon icon = null;
350    
351            int panel = 0;
352            final Tuple.Pair<JComponent, ActionListener> statusComponent = createStatusPanel();
353            tabbedPane.addTab("Status", icon, statusComponent.first,
354                              "Connection and system status");
355            statusComponent.first.setPreferredSize(new Dimension(600, 250));
356            tabbedPane.setMnemonicAt(panel++, KeyEvent.VK_S);
357    
358            final Tuple.Pair<JComponent, ActionListener> chooserComponent = createChooserPanel();
359            tabbedPane.addTab("Choose", icon, chooserComponent.first,
360                              "Choose files/exhibits for upload");
361            tabbedPane.setMnemonicAt(panel++, KeyEvent.VK_C);
362    
363            final Tuple.Pair<JComponent, ActionListener> progressComponent = createProgressPanel();
364            tabbedPane.addTab("Progress", icon, progressComponent.first,
365                              "Progress of uploads to server");
366            tabbedPane.setMnemonicAt(panel++, KeyEvent.VK_P);
367    
368            return(new Tuple.Pair<JComponent,ActionListener>(tabbedPane, new ActionListener(){
369                /**Call the internal listeners of each panel. */
370                public void actionPerformed(final ActionEvent e)
371                    {
372                    statusComponent.second.actionPerformed(e);
373                    chooserComponent.second.actionPerformed(e);
374                    progressComponent.second.actionPerformed(e);
375                    }
376                }));
377            }
378    
379        /**Make the login/status component; never null.
380         * Also creates a listener suitable to poll regularly
381         * from the Swing thread to do async UI updates.
382         */
383        private Tuple.Pair<JComponent, ActionListener> createStatusPanel()
384            {
385            final JPanel pane = new JPanel(new GridBagLayout());
386            final GridBagConstraints c = new GridBagConstraints();
387    
388            // Wrap insets round everything.
389            c.insets = makeStandardInsets();
390    
391            // Heading top (left).
392            c.gridy = 0;
393            c.gridx = 0;
394            c.gridwidth = GridBagConstraints.REMAINDER; // Span all columns.
395            final JLabel heading = new JLabel("<html><big><strong>STATUS</strong></big></html>");
396            pane.add(heading, c);
397            c.gridwidth = 1; // Back to default.
398    
399            // Vertical spacer...
400            ++c.gridy;
401            c.gridx = 0;
402            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
403            pane.add(new JLabel(" "), c);
404            c.gridwidth = 1; // Back to default.
405    
406            // Left-align everything for neatness.
407            c.anchor = GridBagConstraints.WEST;
408    
409            ++c.gridy;
410            c.gridx = 0;
411            final JLabel authInitialsLabel = new JLabel("Your author initials:");
412            pane.add(authInitialsLabel, c);
413            ++c.gridx;
414            // Initialise auth initial field from persistent properties, if any.
415            final JTextField authInitials = new JTextField(logic.props.getUserID(), ExhibitName.MAX_AUTH_INITIALS_LENGTH);
416            authInitials.setMinimumSize(authInitials.getPreferredSize()); // Prevent squeezing!
417            authInitials.setText(logic.props.getUserID()); // Set up with any extant value...
418            pane.add(authInitials, c);
419            authInitialsLabel.setLabelFor(authInitials);
420    
421            // Password below (left).
422            c.gridx = 0;
423            ++c.gridy;
424            final JLabel passwordFieldLabel = new JLabel("Your password:");
425            pane.add(passwordFieldLabel, c);
426            ++c.gridx;
427            final JPasswordField passwordField = new JPasswordField(CoreConsts.MAX_PASSWORD_LEN);
428            passwordField.setMinimumSize(passwordField.getPreferredSize()); // Prevent squeezing!
429            pane.add(passwordField, c);
430            passwordFieldLabel.setLabelFor(passwordField);
431    
432            // Status (on-line or not) below (left).
433            c.gridx = 0;
434            ++c.gridy;
435            final JLabel serverConnectionStatusLabel = new JLabel("Connection status:");
436            pane.add(serverConnectionStatusLabel, c);
437            ++c.gridx;
438            final JLabel serverConnectionStatus = new JLabel("?");
439            pane.add(serverConnectionStatus, c);
440            serverConnectionStatusLabel.setLabelFor(serverConnectionStatus);
441    
442            // Hostname below (left).
443            c.gridx = 0;
444            ++c.gridy;
445            final JLabel serverURLLabel = new JLabel("Server URL:");
446            pane.add(serverURLLabel, c);
447            ++c.gridx;
448            final JLabel serverURL = new JLabel((logic.bs == null) ? "?" :
449                logic.bs.getCodeBase().toString());
450            pane.add(serverURL, c);
451            serverURLLabel.setLabelFor(serverURL);
452    
453            // Time of last successful contact with serverURL.
454            c.gridx = 0;
455            ++c.gridy;
456            final JLabel lastServerContactTimeLabel = new JLabel("Last server contact:");
457            pane.add(lastServerContactTimeLabel, c);
458            ++c.gridx;
459            final JLabel lastServerContactTime = new JLabel("?");
460            pane.add(lastServerContactTime, c);
461            lastServerContactTimeLabel.setLabelFor(lastServerContactTime);
462    
463            // Cached exhibit count in Gallery.
464            c.gridx = 0;
465            ++c.gridy;
466            final JLabel exhibitCountLabel = new JLabel("Gallery exhibit count / update:");
467            pane.add(exhibitCountLabel, c);
468            ++c.gridx;
469            final JLabel exhibitCount = new JLabel("?");
470            pane.add(exhibitCount, c);
471            exhibitCountLabel.setLabelFor(exhibitCount);
472    
473    //        // Count of exhibits in the upload area.
474    //        c.gridx = 0;
475    //        ++c.gridy;
476    //        pane.add(new JLabel("Uploaded exhibit count:"), c);
477    //        ++c.gridx;
478    //        pane.add(new JLabel("?"), c);
479    
480            // Memory usage and total available...
481            c.gridx = 0;
482            ++c.gridy;
483            final JLabel memoryUsageLabel = new JLabel("Memory use/total:");
484            pane.add(memoryUsageLabel, c);
485            ++c.gridx;
486            final JLabel memoryUsage = new JLabel("?");
487            pane.add(memoryUsage, c);
488            memoryUsageLabel.setLabelFor(memoryUsage);
489    
490            // Last dir visited for files (eg to upload).
491            c.gridx = 0;
492            ++c.gridy;
493            final JLabel lastDirLabel = new JLabel("Last directory visited:");
494            pane.add(lastDirLabel, c);
495            ++c.gridx;
496            final JLabel lastDir = new JLabel("?");
497            pane.add(lastDir, c);
498            lastDirLabel.setLabelFor(lastDir);
499    
500            // Application UI last active...
501            c.gridx = 0;
502            ++c.gridy;
503            final JLabel appUILastActiveLabel = new JLabel("UI/app last active:");
504            pane.add(appUILastActiveLabel, c);
505            ++c.gridx;
506            final JLabel appUILastActive = new JLabel("?");
507            pane.add(appUILastActive, c);
508            appUILastActiveLabel.setLabelFor(appUILastActive);
509    
510            // Number of items queued for upload...
511            c.gridx = 0;
512            ++c.gridy;
513            final JLabel queuedCountLabel = new JLabel("Files queued for upload:");
514            pane.add(queuedCountLabel, c);
515            ++c.gridx;
516            final JLabel queuedCount = new JLabel("?");
517            pane.add(queuedCount, c);
518            queuedCountLabel.setLabelFor(queuedCount);
519    
520            // Number of items already uploaded...
521            c.gridx = 0;
522            ++c.gridy;
523            final JLabel uploadedCountLabel = new JLabel("Files uploaded this session:");
524            pane.add(uploadedCountLabel, c);
525            ++c.gridx;
526            final JLabel uploadedCount = new JLabel("?");
527            pane.add(uploadedCount, c);
528            uploadedCountLabel.setLabelFor(uploadedCount);
529    
530            // Transfer speed of items already uploaded...
531            c.gridx = 0;
532            ++c.gridy;
533            final JLabel uploadedSpeedLabel = new JLabel("File upload speed (Bps) this session:");
534            pane.add(uploadedSpeedLabel, c);
535            ++c.gridx;
536            final JLabel uploadedSpeed = new JLabel("?");
537            pane.add(uploadedSpeed, c);
538            uploadedSpeedLabel.setLabelFor(uploadedSpeed);
539    
540            // Vertical spacer...
541            c.gridx = 0;
542            ++c.gridy;
543            pane.add(new JLabel(" "), c);
544    
545            // Start of advanced status...
546            c.gridx = 0;
547            ++c.gridy;
548            pane.add(new JLabel("Advanced..."), c);
549    
550            // JWS status...
551            c.gridx = 0;
552            ++c.gridy;
553            pane.add(new JLabel("JWS Services Available: "), c);
554            ++c.gridx;
555            final StringBuilder sbSrv = new StringBuilder();
556            if(logic.bs != null) { sbSrv.append("Basic "); }
557            if(logic.ps != null) { sbSrv.append("Peristence "); }
558            if(logic.fos != null) { sbSrv.append("FileOpen "); }
559            if(logic.sis != null) { sbSrv.append("SingleInstance "); }
560            if(logic.exs != null) { sbSrv.append("Extended "); }
561            if(sbSrv.length() == 0) { sbSrv.append("-none-"); }
562            pane.add(new JLabel(sbSrv.toString()), c);
563    
564    
565            // Disable the ID/passwd fields until we are sure that we are online.
566            authInitials.setEnabled(false);
567            passwordField.setEnabled(false);
568    
569            // Create a listener to handle async UI-safe updates.
570            final ActionListener al = new ActionListener(){
571                public void actionPerformed(final ActionEvent evt)
572                    {
573                    try
574                        {
575                        final BasicService bs = logic.bs;
576                        if(bs == null)
577                            {
578                            serverConnectionStatus.setText("(not in JWS)");
579                            authInitials.setEnabled(false);
580                            passwordField.setEnabled(false);
581                            }
582                        else
583                            {
584                            // Check if offline.
585                            serverConnectionStatus.setText("checking...");
586                            final boolean offline = bs.isOffline();
587                            serverConnectionStatus.setText(offline ? "off-line" : "on-line");
588                            if(offline)
589                                {
590                                authInitials.setEnabled(false);
591                                passwordField.setEnabled(false);
592                                }
593                            else // We appear to be online.
594                                {
595                                // We can allow the user to enter/alter ID,
596                                // and do running updates of some status fields.
597                                authInitials.setEnabled(true);
598                                passwordField.setEnabled(true);
599    
600                                final char[] pass = passwordField.getPassword();
601                                final String userID = authInitials.getText();
602    
603                                // Save the current ID value in the properties...
604                                logic.props.setUserID(userID);
605    
606                                if(!ExhibitName.validAuthorSyntax(userID))
607                                    {
608                                    logic.setAuthenticationInfo(null, null);
609                                    serverConnectionStatus.setText("To connect, please enter your author initials...");
610                                    }
611                                else if((pass == null) || (pass.length == 0))
612                                    {
613                                    logic.setAuthenticationInfo(null, null);
614                                    serverConnectionStatus.setText("To connect, please enter your password ("+CoreConsts.MIN_PASSWORD_LEN+" to "+CoreConsts.MAX_PASSWORD_LEN+" chars)...");
615                                    }
616                                else if(pass.length < CoreConsts.MIN_PASSWORD_LEN)
617                                    {
618                                    logic.setAuthenticationInfo(null, null);
619                                    serverConnectionStatus.setText("Password too short (min "+CoreConsts.MIN_PASSWORD_LEN+" chars)");
620                                    }
621                                else if(pass.length > CoreConsts.MAX_PASSWORD_LEN)
622                                    {
623                                    logic.setAuthenticationInfo(null, null);
624                                    serverConnectionStatus.setText("Password too long (max "+CoreConsts.MAX_PASSWORD_LEN+" chars)");
625                                    }
626                                else
627                                    {
628                                    // These may be usable authentication values.
629                                    logic.setAuthenticationInfo(userID, pass);
630                                    }
631                                // Clear our password copy to zero.
632                                if(pass != null)
633                                    { for(int i = pass.length; --i >= 0; ) { pass[i] = 0; } }
634    
635                                // Update the last-server-contact time displayed.
636                                if(logic.tunnel != null)
637                                    {
638                                    final long lSCT = logic.tunnel.getLastSuccessfulConnectionTime();
639                                    lastServerContactTime.setText((lSCT == 0) ? "?" :
640                                                                  (new Date(lSCT)).toString());
641                                    if(logic.tunnel.isBroken())
642                                        { serverConnectionStatus.setText("server connection is broken (bad password?)"); }
643                                    }
644                                }
645                            }
646                        }
647                    catch(final Exception e)
648                        {
649                        e.printStackTrace();
650                        serverConnectionStatus.setText("unexpected exception: " + e.getMessage() + ((e.getStackTrace().length > 0) ? (" @ " + e.getStackTrace()[0]) : ""));
651                        }
652    
653                    // Update displayed AEP data.
654                    final AllExhibitProperties aep = logic.getAep();
655                    exhibitCount.setText(((aep.aeid.length == 0) ? "?" :
656                                            String.valueOf(aep.aeid.length)) + " / " +
657                                         ((aep.hashNotChangedSince == 0) ? "?" :
658                                              ((new Date(aep.hashNotChangedSince)).toString())));
659    
660                    // Update memory stats.
661                    final Runtime r = Runtime.getRuntime();
662                    memoryUsage.setText(TextUtils.sizeAsText(r.totalMemory() - r.freeMemory(), true) + " / " +
663                                        TextUtils.sizeAsText(r.totalMemory(), true));
664    
665                    // Update UI activity display.
666                    final long lastActive = logic.getUserLastActive();
667                    if(lastActive == 0)
668                        { appUILastActive.setText("never"); }
669                    else
670                        {
671                        final long t = (System.currentTimeMillis()-lastActive)/1000;
672                        appUILastActive.setText(String.valueOf(t) + "s ago");
673                        if(t < 10) { appUILastActive.setForeground(Color.RED); }
674                        else if(t < 30) { appUILastActive.setForeground(Color.MAGENTA); }
675                        else if(t < 90) { appUILastActive.setForeground(Color.BLUE); }
676                        else { appUILastActive.setForeground(Color.BLACK); }
677                        }
678    
679                    // Update queued-files count.
680                    final int qc = logic.uploadingFiles.size();
681                    queuedCount.setText(String.valueOf(qc));
682                    queuedCount.setForeground((qc == 0) ? Color.BLACK : Color.RED);
683    
684                    // Update uploaded-files count and speed.
685                    uploadedCount.setText(String.valueOf(logic.getCompletedUploadsThisSession()));
686                    uploadedSpeed.setText(String.valueOf(logic.getSmoothedUploadSpeedBps()));
687    
688                    // Update last-directory-visited value.
689                    final File lDir = logic.props.getFileChooserPathHint();
690                    lastDir.setText((lDir == null) ? null : lDir.getPath());
691                    }
692                };
693    
694            return(new Tuple.Pair<JComponent, ActionListener>(pane, al));
695            }
696    
697    
698        /**If true, try to enable file drag-and-drop into the table.
699         * This may not be allowed, eg in the JNLP/JWS sandbox.
700         */
701        private static final boolean ENABLE_FILE_DROP = true;
702    
703        /**Make the chooser/naming component; never null.
704         * Also creates a listener suitable to poll regularly
705         * from the Swing thread to do async UI updates.
706         */
707        private Tuple.Pair<JComponent, ActionListener> createChooserPanel()
708            {
709            // Set it up with any initial AEP state...
710            uib.setAep(logic.getAep());
711            // Initially pretend that there is no "uploader area" set.
712            uib.setUploadAeid(new AllExhibitImmutableData());
713    
714            // Fill in "popular"/likely values for some blank fields.
715            uib.setCommonValuesForUnsetFields();
716    
717            // Create our new pane.
718            final JPanel pane = new JPanel();
719            pane.setLayout(new GridBagLayout());
720            final GridBagConstraints c = new GridBagConstraints();
721    
722            // Wrap insets round everything.
723            c.insets = makeStandardInsets();
724    
725            // Header...
726            c.gridx = 0;
727            c.gridy = 0;
728            pane.add(new JLabel(""), c);
729            ++c.gridx;
730            c.gridwidth = GridBagConstraints.REMAINDER; // Span all columns.
731            final JLabel heading = new JLabel("<html><big><strong>EXHIBIT CHOOSER/NAMER</strong></big></html>");
732            pane.add(heading, c);
733            c.gridwidth = 1; // Back to default.
734    
735            // Status (below)...
736            // Generally we expect to set/update the status asynchronously.
737            ++c.gridy;
738            c.gridx = 0;
739            final JLabel statusLabel = new JLabel("<html><strong>Status:</strong></html>");
740            pane.add(statusLabel, c);
741            statusLabel.setForeground(Color.RED);
742            ++c.gridx;
743            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
744            final JLabel statusText = new JLabel("?");
745            statusText.setForeground(Color.RED);
746            statusText.setBackground(BG_RO_COL);
747            statusText.setOpaque(true);
748            pane.add(statusText, c);
749            c.gridwidth = 1; // Back to default.
750    
751            // Vertical spacer...
752            ++c.gridy;
753            c.gridx = 0;
754            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
755            pane.add(new JLabel(" "), c);
756            c.gridwidth = 1; // Back to default.
757    
758            // FileChooser/dropzone.
759            ++c.gridy;
760            c.gridx = 0;
761            final JLabel fileSelectButtonLabel = new JLabel("File(s):");
762            pane.add(fileSelectButtonLabel, c);
763            ++c.gridx;
764            final String SELECT_FILE = "Select File...";
765            final String SELECT_FILES = "Select File(s)...";
766            final JButton fileSelectButton = new JButton(SELECT_FILE);
767            fileSelectButton.setEnabled(false); // Initially false
768            fileSelectButton.setActionCommand(selectAction.getActionCommand());
769            fileSelectButton.addActionListener(this);
770            fileSelectButton.setToolTipText("Press to choose files to name for upload.");
771            fileSelectButtonLabel.setLabelFor(fileSelectButton);
772            pane.add(fileSelectButton, c);
773            ++c.gridx;
774            final JTable uploadTable = new JTable(uploadTableModel);
775            uploadTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Select just one row at once.
776    //        uploadTable.setToolTipText("List of files you are naming for upload.");
777            uploadTable.setBackground(BG_RO_COL);
778            uploadTable.setOpaque(true);
779            final JScrollPane uploadTableScroller = new JScrollPane(uploadTable);
780            uploadTableScroller.setMinimumSize(new Dimension(2 * fileSelectButtonLabel.getWidth(), 5 * fileSelectButton.getHeight()));
781            uploadTableScroller.setToolTipText("List of files that you are naming for upload.");
782            c.weightx = 1.0; // Expand to take available space.
783            c.weighty = 1.0; // Expand to take available space.
784            c.fill = GridBagConstraints.BOTH; // Expand to take available space.
785            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
786            c.gridheight = 5; // Sit beside 5 items to left.
787            pane.add(uploadTableScroller, c);
788            c.fill = GridBagConstraints.NONE; // Back to default.
789            c.weightx = 0; // Back to default.
790            c.weighty = 0; // Back to default.
791            c.gridwidth = 1; // Back to default.
792            c.gridheight = 1; // Back to default.
793            ++c.gridy;
794            c.gridx = 1;
795            final JButton fileClearAllButton = new JButton("Clear all");
796            fileClearAllButton.setEnabled(false); // Initially false
797            fileClearAllButton.addActionListener(new ActionListener()
798                {
799                /**Clear the "selected files" table if the button is pressed. */
800                public void actionPerformed(final ActionEvent e)
801                    {
802                    logic.selectedFiles.clear();
803                    uploadTableEditRow.set(-1); // Nothing in the table being edited now.
804                    uploadTableModel.fireTableDataChanged();
805                    }
806                });
807            fileClearAllButton.setToolTipText("Clear all selected files from view (no files are deleted).");
808            pane.add(fileClearAllButton, c);
809            ++c.gridy;
810            c.gridx = 1;
811            final JButton fileClearButton = new JButton("Remove");
812            fileClearButton.setEnabled(false); // Initially false
813            fileClearButton.addActionListener(new ActionListener()
814                {
815                /**Clear/remove the current selected row if the button is pressed. */
816                public void actionPerformed(final ActionEvent e)
817                    {
818                    final int selectedRow = uploadTable.getSelectedRow();
819                    if(selectedRow == -1) { return; /* Nothing to do... */ }
820                    uploadTableEditRow.set(-1); // Nothing in the table being edited now.
821                    logic.selectedFiles.remove(selectedRow);
822                    uploadTableModel.fireTableDataChanged();
823                    }
824                });
825            fileClearButton.setToolTipText("Clear the current selected file from view.");
826            pane.add(fileClearButton, c);
827            ++c.gridy;
828            c.gridx = 1;
829            final JButton fileUploadButton = new JButton("Start upload...");
830            fileUploadButton.setEnabled(false); // Initially false
831            fileUploadButton.setToolTipText("Press to start upload...");
832            fileUploadButton.addActionListener(new ActionListener()
833                {
834                public final void actionPerformed(final ActionEvent e)
835                    {
836                    // If any selected file has an error status,
837                    // scroll to it and pop up an error,
838                    // else move all selected files to the upload queue!
839                    for(int i = 0; i < logic.selectedFiles.size(); ++i)
840                        {
841                        final SelectedFileDetails selectedFileDetails = logic.selectedFiles.get(i);
842                        final String errMsg = selectedFileDetails.getStatus(logic);
843                        if(errMsg != null)
844                            {
845                            uploadTable.changeSelection(i, SelectedFileDetails.COLNUM_EXHIBIT_NAME, false, false);
846                            JOptionPane.showMessageDialog(pane, "Please fix: " + errMsg, "problem with selected file", JOptionPane.ERROR_MESSAGE);
847                            return;
848                            }
849                        }
850    
851                    // OK, move all the selected files across to the upload queue!
852                    logger.log("Queueing files for upload: " + logic.selectedFiles.size());
853                    while(logic.selectedFiles.size() > 0)
854                        {
855                        final int last = logic.selectedFiles.size() - 1;
856                        final SelectedFileDetails selectedFileDetails = logic.selectedFiles.get(last);
857                        logic.uploadingFiles.add(selectedFileDetails);
858                        logic.selectedFiles.remove(last);
859                        }
860    
861                    // Redraw the now-empty table.
862                    uploadTableEditRow.set(-1); // Nothing in the table being edited now.
863                    uploadTableModel.fireTableDataChanged();
864                    }
865                });
866            pane.add(fileUploadButton, c);
867            ++c.gridy;
868            c.gridx = 1;
869            final JPanel selectedFilesCountPanel = new JPanel();
870            selectedFilesCountPanel.add(new JLabel("Selected files:"));
871            final JLabel selectedFilesCount = new JLabel("");
872            selectedFilesCountPanel.add(selectedFilesCount);
873            pane.add(selectedFilesCountPanel, c);
874    
875            // Vertical spacer...
876            ++c.gridy;
877            c.gridx = 0;
878            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
879            pane.add(new JLabel(" "), c);
880            c.gridwidth = 1; // Back to default.
881    
882            // Column headings (below)...
883            ++c.gridy;
884            c.gridx = 0;
885            pane.add(new JLabel(" Field "), c);
886            ++c.gridx;
887            ++c.gridx;
888            pane.add(new JLabel(" Value "), c);
889    
890            // Section/Category.
891            ++c.gridy;
892            c.gridx = 0;
893            pane.add(new JLabel("Category:"), c);
894            ++c.gridx;
895            ++c.gridx;
896            // When there is an empty AEP, fill in a default.
897            // This helps with debugging and gets the user started...
898            final JComboBox categoryCombo = new JComboBox(uib.getAllCategories());
899            categoryCombo.setSelectedItem(uib.getCategory());
900            pane.add(categoryCombo, c);
901            // Special rendering of category combobox elements...
902            final class CatCellRenderer extends JLabel implements ListCellRenderer
903                {
904                private static final long serialVersionUID = -5061083427831496368L;
905                CatCellRenderer() { setOpaque(true); }
906                /**Render extension in decorated form using live lookup into DB. */
907                public Component getListCellRendererComponent(final JList list,
908                                                              final Object value,
909                                                              final int index,
910                                                              final boolean isSelected,
911                                                              final boolean cellHasFocus)
912                    {
913                    final String val = (String) value;
914                    final StringBuilder sb = new StringBuilder(64);
915                    if(val != null)
916                        {
917                        sb.append(val);
918                        sb.append(" [").append(GenUtils.computeSectionTitle(logic.getAep(), val, lbb)).append(']');
919                        final Integer c = logic.getAep().getCategoryExhibitCounts().get(val);
920                        if(c != null)
921                            { sb.append(" (").append(c).append(')'); }
922                        sb.append("      "); // FIXME: Hack to stop RHS being chopped off...
923                        }
924                    setText(sb.toString());
925                    setBackground(isSelected ? Color.blue : Color.white);
926                    setForeground(isSelected ? Color.white : Color.black);
927    
928                    // Note user activity...
929                    logic.setUserLastActive("category rendered");
930    
931                    return(this);
932                    }
933                }
934            categoryCombo.setRenderer(new CatCellRenderer());
935            categoryCombo.setToolTipText("Choose which section/category your exhibit belongs in.");
936            pane.add(categoryCombo, c);
937            ++c.gridx;
938            final JLabel categoryDesc = new JLabel();
939            categoryDesc.setToolTipText("Full category/section title.");
940            categoryDesc.setOpaque(true);
941            categoryDesc.setBackground(BG_RO_COL);
942            pane.add(categoryDesc, c);
943            // Update the uib and full title each time the user selects something.
944            final ActionListener cBAL = new ActionListener()
945                {
946                public final void actionPerformed(final ActionEvent e)
947                    {
948                    final Object catItem = categoryCombo.getSelectedItem();
949                    if(catItem instanceof String)
950                        {
951                        final String cat = (String) catItem;
952                        uib.setCategory(cat); // Note new selection...
953                        final String nCat = uib.getCategory();
954                        if(!"".equals(nCat))
955                            { categoryDesc.setText("<html>"+GenUtils.computeSectionTitle(logic.getAep(), nCat, lbb)+"</html>"); }
956                        else
957                            { categoryDesc.setText(null); }
958                        }
959                    else
960                        { categoryDesc.setText(null); }
961    
962                    // Note user activity...
963                    logic.setUserLastActive("category action performed");
964                    }
965                };
966            // Listen for updates.
967            categoryCombo.addActionListener(cBAL);
968            // Show initial value, if any.
969            final String nCat = uib.getCategory();
970            categoryCombo.setSelectedItem(nCat);
971            if(!"".equals(nCat))
972                { categoryDesc.setText(GenUtils.computeSectionTitle(logic.getAep(), nCat, lbb)); }
973    
974            // Main words (stem).
975            ++c.gridy;
976            c.gridx = 0;
977            pane.add(new JLabel("Main Words:"), c);
978            ++c.gridx;
979            ++c.gridx;
980            final int textCols = 48;
981            final JTextArea mainWords = new JTextArea(1+(ExhibitName.MAX_STEM_LENGTH/(textCols*4)), textCols);
982            mainWords.setLineWrap(true);
983            mainWords.setToolTipText("One or more words describing the exhibit, most important first.");
984            c.weighty = 0.5; // Take up some slack vertically...
985    //        c.weightx = 0.5;
986            c.fill = GridBagConstraints.BOTH;
987            pane.add(mainWords, c);
988            c.weighty = 0; // Back to normal.
989            c.weightx = 0;
990            c.fill = GridBagConstraints.NONE;
991            ++c.gridx;
992            final JEditorPane mainWordsResponse = new JEditorPane();
993            final StringBuilder mainWordsResponseText = new StringBuilder(); // Current text displyed.
994            mainWordsResponse.setEditable(false); // Read-only.
995            mainWordsResponse.setContentType("text/html"); // HTML formatted.
996            // Make this the same size as the box to its left by default.
997            mainWordsResponse.setPreferredSize(mainWords.getPreferredSize());
998            mainWordsResponse.setBackground(BG_RO_COL);
999            final JScrollPane mWRSP = new JScrollPane(mainWordsResponse);
1000            mWRSP.setPreferredSize(mainWords.getPreferredSize());
1001            c.weighty = 0.5; // Take up some slack vertically...
1002            c.fill = GridBagConstraints.VERTICAL;
1003            pane.add(mWRSP, c);
1004            c.weighty = 0; // Back to normal.
1005            c.fill = GridBagConstraints.NONE;
1006            ++c.gridx;
1007            // Help appropriately size the upload item list.
1008            uploadTable.setPreferredScrollableViewportSize(new Dimension(2 * mainWords.getPreferredSize().width, Math.max(mainWords.getPreferredSize().height/2, 6 * fileSelectButtonLabel.getPreferredSize().height)));
1009    
1010            // Attributes.
1011            ++c.gridy;
1012            c.gridx = 0;
1013            pane.add(new JLabel("Attributes:"), c);
1014            ++c.gridx;
1015            final Vector<String> attrWords = new Vector<String>();
1016            attrWords.add(ExhibitHandlerBeanBase.SETTER_ALL); // Indicates no attribute.
1017            attrWords.addAll(ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet());
1018            ++c.gridx;
1019            final JPanel attrPane = new JPanel();
1020            c.gridwidth = 2; // Span multiple columns...
1021            pane.add(attrPane, c);
1022            c.gridwidth = 1; // Back to normal...
1023            final int maxAttrs = 4;
1024            final JComboBox attrs[] = new JComboBox[maxAttrs];
1025            final List<String> initialAttrWords = uib.getAttributeWordsAsList();
1026            for(int i = 0; i < maxAttrs; ++i)
1027                {
1028                final boolean isFirst = (i == 0);
1029                // Special rendering of attribute combobox elements...
1030                // Though maybe only for first/last attr combo to save space,
1031                // or only for the selected cell?
1032                final class AttrCellRenderer extends JLabel implements ListCellRenderer
1033                    {
1034                    private static final long serialVersionUID = -4204688614542667708L;
1035                    AttrCellRenderer() { setOpaque(true); }
1036                    /**Render extension in decorated form using live lookup into DB. */
1037                    public Component getListCellRendererComponent(final JList list,
1038                                                                  final Object value,
1039                                                                  final int index,
1040                                                                  final boolean isSelected,
1041                                                                  final boolean cellHasFocus)
1042                        {
1043                        final String val = (String) value;
1044                        if(val != null)
1045                            {
1046                            final StringBuilder sb = new StringBuilder(64);
1047                            sb.append(val);
1048    
1049                            final Map<String,Integer> attrCount = logic.getAep().getExhibitCountsByAttribute();
1050    
1051                            if(isFirst)
1052                                {
1053                                // Insert a localised description if possible.
1054                                final String i18nName = CoreConsts.ATTR_I18N_DESC_PREFIX + val;
1055                                final String msg = lbb.getLocalisedMessage(i18nName);
1056                                if(!msg.equals(i18nName))
1057                                    {
1058                                    sb.append(" [").append(msg).append(']');
1059                                    }
1060    
1061                                // Insert a count if possible.
1062                                final Integer C = attrCount.get(val);
1063                                if((C != null) && (C.intValue() > 0))
1064                                    {
1065                                    final int count = C;
1066                                    sb.append(" (").append(count).append(')');
1067                                    }
1068    
1069                                sb.append("      "); // FIXME: Hack to stop RHS being chopped off...
1070                                setText(sb.toString());
1071                                }
1072                            else
1073                                {
1074                                setText(val); // Very simple rendering...
1075                                }
1076                            }
1077                        else { setText(null); } // No value, so no text.
1078                        setBackground(isSelected ? Color.blue : Color.white);
1079                        setForeground(isSelected ? Color.white : Color.black);
1080    
1081                        // Note user activity...
1082                        logic.setUserLastActive("attributes rendered");
1083    
1084                        return(this);
1085                        }
1086                    }
1087                final JComboBox attrWordsCombo = new JComboBox(attrWords);
1088                attrWordsCombo.setToolTipText("Select an attribute for your exhibit.");
1089                attrWordsCombo.setRenderer(new AttrCellRenderer());
1090                // Capture any existing value.
1091                if(initialAttrWords.size() > i)
1092                    { attrWordsCombo.setSelectedItem(initialAttrWords.get(i)); }
1093                attrs[i] = attrWordsCombo;
1094                attrPane.add(attrWordsCombo, c);
1095                }
1096    
1097            // Number in series.
1098            ++c.gridy;
1099            c.gridx = 0;
1100            pane.add(new JLabel("Number in Series:"), c);
1101            ++c.gridx;
1102            ++c.gridx;
1103            final SpinnerModel numberInSeriesModel = new SpinnerNumberModel(
1104                                           uib.getNumber(), // initial value
1105                                           0,    // min
1106                                           9999, // max
1107                                           1);   // step
1108            final JSpinner nos = new JSpinner(numberInSeriesModel);
1109            pane.add(nos, c);
1110            nos.setEditor(new JSpinner.NumberEditor(nos, "#"));
1111            nos.setToolTipText("Number-in-series value for exhibit, or 0 if none.");
1112    
1113            // Type/extension.
1114            ++c.gridy;
1115            c.gridx = 0;
1116            pane.add(new JLabel("Type/suffix:"), c);
1117            ++c.gridx;
1118            final JPanel ap = new JPanel();
1119            final JLabel apl = new JLabel("Auto:");
1120            ap.add(apl, c);
1121            ap.add(autoSuffixCheckBox, c);
1122            autoSuffixCheckBox.setSelected(true); // Default to "auto" mode.
1123            autoSuffixCheckBox.setToolTipText("In 'auto' mode, the uploader can try to guess the type of each exhibit that you select.");
1124            apl.setLabelFor(autoSuffixCheckBox);
1125            pane.add(ap, c);
1126            ++c.gridx;
1127            final Vector<String> extensions = new Vector<String>();
1128            for(final ExhibitMIME.ExhibitTypeParameters etp : ExhibitMIME.getAllValidExhibitTypes())
1129                { extensions.add(etp.dotSuffixForInputFile); }
1130            Collections.sort(extensions);
1131            final JComboBox fileType = new JComboBox(extensions);
1132            // Special rendering of combobox elements...
1133            final class TypeCellRenderer extends JLabel implements ListCellRenderer
1134                {
1135                private static final long serialVersionUID = 5537423282374537074L;
1136                TypeCellRenderer() { setOpaque(true); }
1137                /**Render extension in decorated form using live lookup into DB. */
1138                public Component getListCellRendererComponent(final JList list,
1139                                                              final Object value,
1140                                                              final int index,
1141                                                              final boolean isSelected,
1142                                                              final boolean cellHasFocus)
1143                    {
1144                    final String val = (String) value;
1145                    final StringBuilder sb = new StringBuilder(64);
1146                    if(val != null)
1147                        {
1148                        sb.append(val);
1149                        final ExhibitMIME.ExhibitTypeParameters etp = ExhibitMIME.getExhibitType(val.substring(1));
1150                        if(etp != null)
1151                            {
1152                            sb.append(" [" + etp.mimeType + ": " + etp.description + "]");
1153                            final Integer c = logic.getAep().getDottedExtensionExhibitCounts().get(val);
1154                            if(c != null)
1155                                { sb.append(" (").append(c).append(')'); }
1156                            }
1157                        sb.append("      "); // FIXME: Hack to stop RHS being chopped off...
1158                        }
1159                    setText(sb.toString());
1160                    setBackground(isSelected ? Color.blue : Color.white);
1161                    setForeground(isSelected ? Color.white : Color.black);
1162    
1163                    // Note user activity...
1164                    logic.setUserLastActive("exhibit type rendered");
1165    
1166                    return(this);
1167                    }
1168                }
1169            fileType.setRenderer(new TypeCellRenderer());
1170            fileType.setSelectedItem(uib.getSuffix()); // Select existing value, if any.
1171            fileType.setToolTipText("Type/extension of your exhibit, eg .jpg for a JPEG image.");
1172            pane.add(fileType, c);
1173            ++c.gridx;
1174            final JLabel fileExt = new JLabel(uib.getSuffix());
1175            fileExt.setBackground(BG_RO_COL);
1176            fileExt.setOpaque(true);
1177            pane.add(fileExt, c);
1178    
1179            // Author.
1180            ++c.gridy;
1181            c.gridx = 0;
1182            pane.add(new JLabel("Author:"), c);
1183            ++c.gridx;
1184            ++c.gridx;
1185            final JLabel auth = new JLabel(logic.props.getUserID());
1186            auth.setBackground(BG_RO_COL);
1187            auth.setOpaque(true);
1188            pane.add(auth, c);
1189            ++c.gridx;
1190            final JLabel authDesc = new JLabel();
1191            authDesc.setBackground(BG_RO_COL);
1192            authDesc.setOpaque(true);
1193            pane.add(authDesc, c);
1194    
1195            // Description.
1196            ++c.gridy;
1197            c.gridx = 0;
1198            pane.add(new JLabel("Description:"), c);
1199            ++c.gridx;
1200            ++c.gridx;
1201            final JTextArea description = new JTextArea(1+(Math.min(CoreConsts.DESCRIPTION_MAX_CHARS/4, 256)/textCols), textCols);
1202            description.setLineWrap(true);
1203            description.setWrapStyleWord(true);
1204            description.setToolTipText("Optional plain ASCII description of the exhibit, possibly with HTML markup, maximum " + CoreConsts.DESCRIPTION_MAX_CHARS + " characters.");
1205            c.weighty = 0.5; // Take up some slack vertically...
1206    //        c.weightx = 0.5;
1207            c.fill = GridBagConstraints.BOTH;
1208            pane.add(description, c);
1209            c.weighty = 0; // Back to normal.
1210            c.weightx = 0;
1211            c.fill = GridBagConstraints.NONE;
1212            ++c.gridx;
1213            final JEditorPane descriptionResponse = new JEditorPane();
1214            final StringBuilder descriptionText = new StringBuilder(); // Current text displyed.
1215            descriptionResponse.setEditable(false); // Read-only.
1216            descriptionResponse.setContentType("text/html"); // HTML formatted.
1217            descriptionResponse.setBackground(BG_RO_COL);
1218            // Make this the same size as the box to its left by default.
1219            descriptionResponse.setPreferredSize(description.getPreferredSize());
1220            final JScrollPane dSP = new JScrollPane(descriptionResponse);
1221            dSP.setPreferredSize(description.getPreferredSize());
1222            c.weighty = 0.5; // Take up some slack vertically...
1223            c.fill = GridBagConstraints.VERTICAL;
1224            pane.add(dSP, c);
1225            c.weighty = 0; // Back to normal.
1226            c.fill = GridBagConstraints.NONE;
1227    
1228    
1229            // Ask to be notified of selection changes in the table...
1230            final ListSelectionModel rowSM = uploadTable.getSelectionModel();
1231            rowSM.addListSelectionListener(new ListSelectionListener()
1232                {
1233                public final void valueChanged(final ListSelectionEvent e)
1234                    {
1235                    final ListSelectionModel lsm =
1236                        (ListSelectionModel) e.getSource();
1237                    if(!lsm.isSelectionEmpty())
1238                        {
1239                        final int selectedRow = lsm.getMinSelectionIndex();
1240                        // Make this edit row available to the rest of the UI.
1241                        uploadTableEditRow.set(selectedRow);
1242                        // Make sure that this row is visible.
1243                        uploadTable.changeSelection(selectedRow, SelectedFileDetails.COLNUM_EXHIBIT_NAME, false, false);
1244    
1245                        // Attempt to set editor fields from selected entry...
1246                        Name.ExhibitFull name = null;
1247                        Name.ExhibitFull prevName = null, nextName = null;
1248                        String desc = null;
1249                        synchronized(logic.selectedFiles) // Avoid races...
1250                            {
1251                            if((selectedRow >= 0) && (selectedRow < logic.selectedFiles.size()))
1252                                {
1253                                name = logic.selectedFiles.get(selectedRow).getEsa().getExhibitFullName();
1254                                desc = logic.selectedFiles.get(selectedRow).getDescription();
1255                                }
1256                            // Get the names on either side.
1257                            if((selectedRow >= +1) && (selectedRow < logic.selectedFiles.size()+1))
1258                                { prevName = logic.selectedFiles.get(selectedRow-1).getEsa().getExhibitFullName(); }
1259                            if((selectedRow >= -1) && (selectedRow < logic.selectedFiles.size()-1))
1260                                { nextName = logic.selectedFiles.get(selectedRow+1).getEsa().getExhibitFullName(); }
1261                            }
1262                        // Don't set the name to one of the values on either side.
1263                        // This should avoid the most obvious finger trouble.
1264                        if((name != null) &&
1265                           !name.equals(prevName) && !name.equals(nextName) &&
1266                           (desc != null))
1267                            {
1268                            // Set the name as best we can.
1269                            uib.setFullName(name.toString()); // Preserve String-based API for bean.
1270                            uib.setDescription(desc);
1271    
1272                            // Set UI editor fields individually...
1273                            categoryCombo.setSelectedItem(ExhibitName.getCategoryComponent(name));
1274                            final SortedSet<String> legalAttrs = ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet();
1275                            mainWords.setText(name.getShortName().getMainWordsComponent(legalAttrs).toString());
1276                            final Enumeration<?> attrEn = ExhibitName.getAttributeWordsComponentEnumeration(name, legalAttrs);
1277                            for(int i = 0; i < maxAttrs; ++i)
1278                                {
1279                                if((attrEn != null) && attrEn.hasMoreElements())
1280                                    { attrs[i].setSelectedItem(attrEn.nextElement()); }
1281                                else
1282                                    { attrs[i].setSelectedItem(ExhibitHandlerBeanBase.SETTER_ALL); }
1283                                }
1284                            numberInSeriesModel.setValue(Integer.valueOf(ExhibitName.getNumberInSeriesComponent(name)));
1285                            auth.setText(ExhibitName.getAuthorComponent(name).toString());
1286                            fileType.setSelectedItem("." + ExhibitName.getExtensionComponent(name));
1287                            description.setText(desc);
1288                            }
1289    //logger.log("SELECTION INDEX/NAME: " + selectedRow + '/' + name);
1290                        }
1291                    }
1292                });
1293    
1294    
1295            // Attach file-drop handler to uploadTable / button / pane if possible.
1296            if(ENABLE_FILE_DROP)
1297                {
1298                final TransferHandler uTTH =
1299                    new FileTransferHandler(autoSuffixCheckBox,
1300                                            uploadTableModel,
1301                                            logic,
1302                                            uib);
1303    //            uploadTable.setDragEnabled(false); // Drop target only...
1304                uploadTable.setTransferHandler(uTTH);
1305                pane.setTransferHandler(uTTH);
1306                pane.setToolTipText("Drop your new exhibit files on this panel...");
1307                fileSelectButton.setTransferHandler(uTTH);
1308                fileSelectButton.setToolTipText("Press to choose files to name for upload, or drag the files here.");
1309                }
1310    
1311    
1312            // Create a listener to handle async UI-safe updates.
1313            final ActionListener al = new ActionListener(){
1314                public final void actionPerformed(final ActionEvent evt)
1315                    {
1316                    // If the AEP has changed
1317                    // then reset the category combo box
1318                    // and fill in any blank fields we can guess.
1319                    final AllExhibitProperties aep = logic.getAep();
1320                    if(aep.longHash != uib.getAep().longHash)
1321                        {
1322                        uib.setAep(aep);
1323                        uib.setCommonValuesForUnsetFields();
1324    
1325                        categoryCombo.setModel(new DefaultComboBoxModel(uib.getAllCategories()));
1326                        categoryCombo.setSelectedItem(uib.getCategory());
1327                        fileType.setSelectedItem(uib.getSuffix());
1328                        }
1329    
1330                    // Allow table clear iff something in the table to be cleared!
1331                    final int numberOfSelectedFiles = logic.selectedFiles.size();
1332                    fileClearAllButton.setEnabled(numberOfSelectedFiles != 0);
1333    
1334                    // Allow row clear iff something selected in the table!
1335                    fileClearButton.setEnabled(uploadTable.getSelectedRow() != -1);
1336    
1337                    // Allow "upload" if there is something available.
1338                    // However, a dialogue may get popped up
1339                    // if an error is encountered elsewhere.
1340                    fileUploadButton.setEnabled(numberOfSelectedFiles != 0);
1341    
1342                    // Show correct file count...
1343                    selectedFilesCount.setText(String.valueOf(numberOfSelectedFiles));
1344    
1345                    // Renormalise main-words component and show any warnings.
1346                    // To avoid some ugly things,
1347                    // such as partial words being taken as attributes,
1348                    // and to save a bit of CPU and time,
1349                    // only do an update when the user enters a separator
1350                    // or the text area looses focus
1351                    // or the user has been paused a long time.
1352                    //
1353                    // Upon change of the main words
1354                    // we reset the number-in-sequence to 1
1355                    // to avoid accidentally carrying over
1356                    // a non-1-value from a block upload
1357                    // to an unrelated subsequent upload.
1358                    // TODO: only do this if there are no exhibits with the same main words uploaded.
1359                    final String currentMainWords = mainWords.getText();
1360                    final int mainWordsCaretPosition = mainWords.getCaretPosition();
1361                    final boolean mWHF = mainWords.hasFocus();
1362                    final boolean trailingSep =
1363                        currentMainWords.endsWith(" ") ||
1364                        currentMainWords.endsWith(ExhibitName.WORD_SEPS);
1365                    if(/* trailingSep || */ !mWHF ||
1366                       (System.currentTimeMillis() - logic.getUserLastActive() > TEXT_ENTRY_TIMEOUT_MS)) // User paused a long time!
1367                        {
1368                        // If completed text is definitely not normalised
1369                        // then replace it with a normalised version.
1370                        // Make sure that we do not zap trailing space/separator
1371                        // while user is typing in a name.
1372                        final String currentWordsTrimmed = (trailingSep ?
1373                            currentMainWords.substring(0, currentMainWords.length()-1) :
1374                            currentMainWords).trim();
1375                        if(!currentWordsTrimmed.equals(uib.getMainWords()))
1376                            {
1377                            uib.setMainWords(currentMainWords);
1378    
1379                            // Note user activity...
1380                            logic.setUserLastActive("main words updated/normalised");
1381    
1382                            // Changing the main words resets the number-in-sequence.
1383                            if(((Number) nos.getValue()).intValue() != 1)
1384                                {
1385                                nos.setValue(Integer.valueOf(1));
1386                                uib.setNumber(1);
1387                                }
1388                            }
1389                        final String normMainWords = uib.getMainWords();
1390                        if(!currentWordsTrimmed.equals(normMainWords))
1391                            {
1392                            mainWords.setText(trailingSep ?
1393                                (normMainWords + ExhibitName.WORD_SEP) : normMainWords);
1394                            // Try to restore caret position...
1395                            // Ignore complaints about invalid position...
1396                            try { mainWords.setCaretPosition(mainWordsCaretPosition); }
1397                            catch(final IllegalArgumentException e) { }
1398                            }
1399                        // Generate warning message for new/bad words,
1400                        // info about information available, location, etc.
1401                        final StringBuilder mainWordsMsg = new StringBuilder();
1402                        final String newWords = uib.getNewMainWords();
1403                        if(newWords.length() != 0)
1404                            {
1405                            // Do warning about unrecognised words...
1406                            final String localisedMessage = lbb.getLocalisedMessage("uploader.newWordsWarning", newWords);
1407                            mainWordsMsg.append(localisedMessage);
1408                            }
1409                        // Add any available section and tree-structured description.
1410                        // We can do this only if we have the category and some words.
1411                        // Do this mostly as would be shown in a catalogue page.
1412                        final String cat = uib.getCategory();
1413                        if(!"".equals(cat))
1414                            {
1415                            // We'll see if there is descriptive text for this category.
1416                            final CharSequence catDesc = GenUtils.getLocalisedSectionDesc(aep, cat, lbb);
1417                            // We show the localised version if it exists and isn't just in a different case.
1418                            if(catDesc != null)
1419                                {
1420                                mainWordsMsg.append("<p><strong>");
1421                                mainWordsMsg.append(lbb.getLocalisedMessage("common.cat.sectionDescription"));
1422                                mainWordsMsg.append("</strong></p>");
1423                                mainWordsMsg.append("<p>");
1424                                mainWordsMsg.append(catDesc);
1425                                mainWordsMsg.append("</p><hr>");
1426                                }
1427    
1428                            if(!"".equals(normMainWords))
1429                                {
1430                                // Synthesise a fake exhibit name for tree-data lookup.
1431                                final String synthExhibitName = cat + '/' + normMainWords +
1432                                                                   "-A.a";
1433                                final CharSequence treeDesc = GenUtils.getLocalisedTreeDesc(aep,
1434                                                                                      synthExhibitName,
1435                                                                                      lbb, true, true, true, false);
1436                                if(treeDesc.length() > 0)
1437                                    {
1438                                    mainWordsMsg.append("<p><ul>");
1439                                    mainWordsMsg.append(treeDesc);
1440                                    mainWordsMsg.append("</ul></p>");
1441                                    }
1442                                // Show any new-words warning at the end (again)
1443                                // so that it is hard to miss.
1444                                if(newWords.length() != 0)
1445                                    {
1446                                    // Do warning about unrecognised words...
1447                                    final String localisedMessage = lbb.getLocalisedMessage("uploader.newWordsWarning", newWords);
1448                                    mainWordsMsg.append(localisedMessage);
1449                                    }
1450                                }
1451                            }
1452                        // Now display the main-words message (if any)...
1453                        final String mWPText = mainWordsMsg.toString();
1454                        if((mainWordsResponseText.length() != mWPText.length()) ||
1455                           !mWPText.equals(mainWordsResponseText.toString()))
1456                            {
1457                            mainWordsResponseText.setLength(0);
1458                            mainWordsResponseText.append(mWPText);
1459                            mainWordsResponse.setDocument(mainWordsResponse.getEditorKit().createDefaultDocument());
1460                            mainWordsResponse.setText(mWPText);
1461    
1462                            // Note user activity...
1463                            logic.setUserLastActive("main words updated");
1464                            }
1465                        }
1466    
1467                    // Keep the attributes in sync.
1468                    final List<String> attrWordsInUI = new ArrayList<String>(maxAttrs);
1469                    for(int i = 0; i < maxAttrs; ++i)
1470                        {
1471                        final Object selectedItem = attrs[i].getSelectedItem();
1472                        if((selectedItem instanceof String) &&
1473                           !selectedItem.equals(ExhibitHandlerBeanBase.SETTER_ALL))
1474                            { attrWordsInUI.add((String) selectedItem); }
1475                        }
1476                    final List<String> a1 = uib.getAttributeWordsAsList();
1477                    if(!attrWordsInUI.equals(a1))
1478                        {
1479                        // Copy new attrs into uib...
1480                        uib.setAttributeWords(attrWordsInUI);
1481    
1482                        // Note user activity...
1483                        logic.setUserLastActive("attributes updated");
1484                        }
1485                    // Copy normalised (uib) value back to UI.
1486                    final List<String> attributeWordsAsList = uib.getAttributeWordsAsList();
1487                    for(int i = 0; i < maxAttrs; ++i)
1488                        {
1489                        // Clear any trailing values completely...
1490                        if(i >= attributeWordsAsList.size())
1491                            { attrs[i].setSelectedItem(ExhibitHandlerBeanBase.SETTER_ALL); }
1492                        else
1493                            { attrs[i].setSelectedItem(attributeWordsAsList.get(i)); }
1494                        }
1495    
1496                    // Keep the number-in-series field in sync.
1497                    final int nosGUIValue = ((Number) nos.getValue()).intValue();
1498                    if(nosGUIValue != uib.getNumber())
1499                        {
1500                        // Copy UI value into uib.
1501                        uib.setNumber(nosGUIValue);
1502    
1503                        // Note user activity...
1504                        logic.setUserLastActive("number-in-series updated");
1505                        }
1506                    // Copy normalised (uib) value back to UI.
1507                    nos.setValue(Integer.valueOf(uib.getNumber()));
1508                    // Indicate selection of "file" or "files" as appropriate...
1509                    fileSelectButton.setText((uib.getNumber() == 0) ? SELECT_FILE : SELECT_FILES);
1510    
1511                    // Keep the author field in sync.
1512                    uib.setAuthor(logic.props.getUserID());
1513                    final String authorNorm = uib.getAuthor();
1514                    auth.setText(authorNorm);
1515                    final GenProps.AuthData authData = logic.getGenProps().getAuthData(authorNorm);
1516                    authDesc.setText((authData == null) ? null : "<html>"+authData.name+"</html>");
1517    
1518                    // Keep the extension/type field in sync.
1519                    uib.setSuffix((String) fileType.getSelectedItem());
1520                    final String suffixNorm = uib.getSuffix();
1521                    fileType.setSelectedItem(suffixNorm);
1522                    fileExt.setText(suffixNorm);
1523    
1524                    // Render any description text as HTML.
1525                    final String desc = description.getText();
1526                    final int descCaretPosition = description.getCaretPosition();
1527                    final boolean trailingS = desc.endsWith(" ");
1528                    uib.setDescription(desc);
1529                    final String descNorm = uib.getDescription();
1530                    if(!descNorm.equals(desc))
1531                        {
1532                        // Normalise input text...
1533                        // (IFF user is not still typing!)
1534                        if(!description.hasFocus() ||
1535                           (System.currentTimeMillis() - logic.getUserLastActive() > TEXT_ENTRY_TIMEOUT_MS))
1536                            {
1537                            description.setText(trailingS ?
1538                                (descNorm + ' ') : descNorm);
1539                            // Try to put the cursor back where it was...
1540                            try { description.setCaretPosition(descCaretPosition); }
1541                            catch(final IllegalArgumentException e) { } // Ignore out-of-range complaints.
1542    
1543                            // Note user activity...
1544                            logic.setUserLastActive("description updated/normalised");
1545                            }
1546                        }
1547                    if((descriptionText.length() != descNorm.length()) ||
1548                       !descNorm.equals(descriptionText.toString()))
1549                        {
1550                        descriptionResponse.setDocument(descriptionResponse.getEditorKit().createDefaultDocument());
1551                        descriptionResponse.setText(descNorm);
1552    
1553                        descriptionText.setLength(0);
1554                        descriptionText.append(descNorm);
1555                        descriptionResponse.setDocument(descriptionResponse.getEditorKit().createDefaultDocument());
1556                        descriptionResponse.setText(descNorm);
1557    
1558                        // Note user activity...
1559                        logic.setUserLastActive("description updated");
1560                        }
1561    
1562    
1563                    // Can we allow selection of new files to upload?
1564                    if(JWS_UPLOAD_ONLY && (logic.fos == null))
1565                        {
1566                        statusLabel.setForeground(Color.RED);
1567                        statusText.setForeground(Color.RED);
1568                        fileSelectButton.setEnabled(false); // Disable upload.
1569                        statusText.setText("No JWS FileOpenService available; cannot upload, sorry.");
1570                        }
1571                    // Check the status of the uib;
1572                    // have we enough info to allow an upload to proceed
1573                    // and is the name unique in all our databases?
1574                    else if(uib.enoughValidUniqueInfo() &&
1575                            !logic.uploadingFiles.shortNameInUse(uib.getName(false)))
1576                        {
1577                        // All OK!
1578                        // Potentially enable the upload stage.
1579                        statusLabel.setForeground(Color.GREEN);
1580                        statusText.setForeground(Color.GREEN);
1581                        statusText.setText("<html>" + lbb.getLocalisedMessage("uploader.nameOK", uib.getFullName()) + "</html>");
1582                        fileSelectButton.setEnabled(true); // Enable upload.
1583                        }
1584                    else
1585                        {
1586                        // Not enough info yet, or a conflict.
1587                        // Show the error and disable upload stage.
1588                        statusLabel.setForeground(Color.RED);
1589                        statusText.setForeground(Color.RED);
1590                        fileSelectButton.setEnabled(false); // Disable file selection.
1591    
1592                        // Try to provide specific error info/clues.
1593                        if(!uib.enoughInfo())
1594                            {
1595                            statusText.setText("<html>" + lbb.getLocalisedMessage("uploader.notEnoughInfo") + "</html>");
1596                            }
1597                        else if(!uib.canMakeValidName())
1598                            {
1599                            statusText.setText("<html>" + lbb.getLocalisedMessage("uploader.notValidName") + "</html>");
1600                            }
1601                        else if(!uib.nameUniqueInMainDatabase())
1602                            {
1603                            statusText.setText("<html>" + lbb.getLocalisedMessage("uploader.nameExistsLive", uib.getFullName()) + "</html>");
1604                            }
1605                        else if(!uib.nameUniqueInAuxDatabase() || logic.uploadingFiles.shortNameInUse(uib.getName(false)))
1606                            {
1607                            statusText.setText("<html>" + lbb.getLocalisedMessage("uploader.nameExistsUpload", uib.getFullName()) + "</html>");
1608                            }
1609                        else
1610                            {
1611                            // Generic error message...
1612                            statusText.setText("<html>" + lbb.getLocalisedMessage("uploader.nameCannotBeUsed", uib.getFullName()) + "</html>");
1613                            }
1614                        }
1615    
1616    
1617                    // If an entry in the table is marked for editing,
1618                    // then copy any updated name back into it
1619                    // provided that the putative new name is valid and unique.
1620                    // Also deal with any updated description.
1621                    final int editRow = uploadTableEditRow.get();
1622                    final String fullName = uib.getFullName();
1623                    synchronized(logic.selectedFiles) // Avoid races...
1624                        {
1625                        if(ExhibitName.validNameSyntax(fullName) &&
1626                           (editRow >= 0) && (editRow < logic.selectedFiles.size()))
1627                            {
1628                            final SelectedFileDetails selectedFileDetails = logic.selectedFiles.get(editRow);
1629    
1630                            final String shortName = ExhibitName.getFileComponent(fullName).toString();
1631    
1632                            // If this short name is valid and unique
1633                            // then it also implies that it has changed...
1634                            if(logic.shortNameValidAndUnique(shortName))
1635                                {
1636                                try
1637                                    {
1638                                    // Something changed; do an update...
1639                                    logic.selectedFiles.setExhibitName(editRow, fullName);
1640                                    // ...make sure the changed item is visible...
1641                                    uploadTable.changeSelection(editRow, SelectedFileDetails.COLNUM_EXHIBIT_NAME, false, false);
1642                                    // ...and get the display redrawn.
1643                                    uploadTableModel.fireTableDataChanged();
1644                                    }
1645                                catch(final IllegalArgumentException e)
1646                                    {
1647                                    // Silently ignore attempts to set duplicate names...
1648                                    }
1649                                }
1650    
1651                            // Keep the description in sync too...
1652                            if(!descNorm.equals(selectedFileDetails.getDescription()))
1653                                {
1654                                // Something changed; do an update...
1655                                logic.selectedFiles.setDescription(editRow, descNorm);
1656                                // ...make sure the changed item is visible...
1657                                uploadTable.changeSelection(editRow, SelectedFileDetails.COLNUM_DESCRIPTION, false, false);
1658                                // ...and get the display redrawn.
1659                                uploadTableModel.fireTableDataChanged();
1660                                }
1661                            }
1662                        }
1663                    }
1664                };
1665    
1666            return(new Tuple.Pair<JComponent, ActionListener>(
1667                new JScrollPane(pane), al));
1668            }
1669    
1670    
1671        /**Make the progress component; never null.
1672         * Also creates a listener suitable to poll regularly
1673         * from the Swing thread to do async UI updates.
1674         */
1675        private Tuple.Pair<JComponent, ActionListener> createProgressPanel()
1676            {
1677            // Create our new pane.
1678            final JPanel pane = new JPanel();
1679            pane.setLayout(new GridBagLayout());
1680            final GridBagConstraints c = new GridBagConstraints();
1681    
1682            // Wrap insets round everything.
1683            c.insets = makeStandardInsets();
1684    
1685            // Header...
1686            c.gridx = 0;
1687            c.gridy = 0;
1688            pane.add(new JLabel(""), c);
1689            ++c.gridx;
1690            c.gridwidth = GridBagConstraints.REMAINDER; // Span all columns.
1691            final JLabel heading = new JLabel("<html><big><strong>UPLOAD PROGRESS</strong></big></html>");
1692            pane.add(heading, c);
1693            c.gridwidth = 1; // Back to default.
1694    
1695            // Vertical spacer...
1696            ++c.gridy;
1697            c.gridx = 0;
1698            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
1699            pane.add(new JLabel(" "), c);
1700            c.gridwidth = 1; // Back to default.
1701    
1702            // Status line...
1703            ++c.gridy;
1704            c.gridx = 0;
1705            pane.add(new JLabel("So far:"), c);
1706            ++c.gridx;
1707            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
1708            final JPanel statusLine = new JPanel();
1709            statusLine.setOpaque(true);
1710            statusLine.setBackground(Color.YELLOW);
1711            statusLine.setMinimumSize(heading.getPreferredSize());
1712            final JLabel uploadedLabel = new JLabel("Uploaded:");
1713            final JLabel uploadedCount = new JLabel("0");
1714            uploadedLabel.setLabelFor(uploadedCount);
1715            statusLine.add(uploadedLabel);
1716            statusLine.add(uploadedCount);
1717            final JLabel uploadSpeedLabel = new JLabel("  Upload speed:");
1718            final JLabel uploadSpeedValue = new JLabel("?");
1719            uploadSpeedLabel.setLabelFor(uploadSpeedValue);
1720            statusLine.add(uploadSpeedLabel);
1721            statusLine.add(uploadSpeedValue);
1722            final JLabel uploadCompleteLabel = new JLabel("  Time to finish:");
1723            final String TEXT_NA = "N/A";
1724            final JLabel uploadCompleteValue = new JLabel(TEXT_NA);
1725            uploadCompleteLabel.setLabelFor(uploadCompleteValue);
1726            statusLine.add(uploadCompleteLabel);
1727            statusLine.add(uploadCompleteValue);
1728            pane.add(statusLine, c);
1729            c.gridwidth = 1; // Back to default.
1730    
1731            // "Uploading now" slots...
1732            ++c.gridy;
1733            c.gridx = 0;
1734    //        pane.add(new JLabel("Uploading now:"), c);
1735            ++c.gridx;
1736            pane.add(new JLabel("Name"), c);
1737            ++c.gridx;
1738            pane.add(new JLabel("Size"), c);
1739            ++c.gridx;
1740            pane.add(new JLabel("Progress"), c);
1741            final class UploadingNowItem
1742                {
1743                final JTextField nameBox = new JTextField("", 20);
1744                final JTextField sizeBox = new JTextField("", 10);
1745                final JProgressBar progressBar = new JProgressBar();
1746                }
1747            // Implicitly each index in uploadingNow[]
1748            // is associated with the same index in logic.inProgressSlots[].
1749            final UploadingNowItem uploadingNow[] = new UploadingNowItem[UploaderLogic.MAX_CONC_UPLOADS];
1750            for(int i = 0; i < UploaderLogic.MAX_CONC_UPLOADS; ++i)
1751                {
1752                ++c.gridy;
1753                c.gridx = 0;
1754                pane.add(new JLabel("Uploading now:"), c);
1755                ++c.gridx;
1756                final UploadingNowItem uni = new UploadingNowItem();
1757                uploadingNow[i] = uni;
1758                uni.nameBox.setEditable(false); // Display only.
1759                uni.nameBox.setMinimumSize(heading.getPreferredSize());
1760                pane.add(uni.nameBox, c);
1761                ++c.gridx;
1762                uni.sizeBox.setEditable(false); // Display only.
1763                uni.sizeBox.setMinimumSize(uni.sizeBox.getPreferredSize());
1764                pane.add(uni.sizeBox, c);
1765                ++c.gridx;
1766                pane.add(uni.progressBar, c);
1767                uni.progressBar.setStringPainted(true); // Show a string.
1768                uni.progressBar.setString(""); // Hide % string.
1769                uni.progressBar.setMinimumSize(heading.getPreferredSize());
1770                }
1771    
1772            // Vertical spacer...
1773            ++c.gridy;
1774            c.gridx = 0;
1775            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
1776            pane.add(new JLabel(" "), c);
1777            c.gridwidth = 1; // Back to default.
1778    
1779            // Upload queue
1780            ++c.gridy;
1781            c.gridx = 0;
1782            final JLabel queueLabel = new JLabel("Upload queue:");
1783            pane.add(queueLabel, c);
1784            ++c.gridx;
1785            final JTable queueTable = new JTable(queueTableModel);
1786            queueTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Select just one row at once.
1787    //        queueTable.setToolTipText("List of files queued for upload.");
1788            queueTable.setBackground(BG_RO_COL);
1789            queueTable.setOpaque(true);
1790            final JScrollPane queueTableScroller = new JScrollPane(queueTable);
1791            queueTableScroller.setMinimumSize(new Dimension(2 * heading.getWidth(), 10 * heading.getHeight()));
1792            queueTableScroller.setToolTipText("List of files queued for upload.");
1793            c.weightx = 1.0; // Expand to take available space.
1794            c.weighty = 1.0; // Expand to take available space.
1795            c.fill = GridBagConstraints.BOTH; // Expand to take available space.
1796            c.gridwidth = GridBagConstraints.REMAINDER; // Span all remaining columns.
1797            c.gridheight = 5; // Sit beside various items to left.
1798            pane.add(queueTableScroller, c);
1799            c.fill = GridBagConstraints.NONE; // Back to default.
1800            c.weightx = 0; // Back to default.
1801            c.weighty = 0; // Back to default.
1802            c.gridwidth = 1; // Back to default.
1803            c.gridheight = 1; // Back to default.
1804            ++c.gridy;
1805            c.gridx = 0;
1806            pane.add(new JLabel(" "), c); // Spacer...
1807            ++c.gridy;
1808            c.gridx = 0;
1809            final JButton fileClearButton = new JButton("Discard");
1810            fileClearButton.setEnabled(false); // Initially false.
1811            fileClearButton.addActionListener(new ActionListener()
1812                {
1813                /**Clear/remove the current selected row if the button is pressed. */
1814                public void actionPerformed(final ActionEvent e)
1815                    {
1816                    final int selectedRow = queueTable.getSelectedRow();
1817                    if(selectedRow == -1) { return; /* Nothing to do... */ }
1818                    final SelectedFileDetails selectedFileDetails = logic.uploadingFiles.get(selectedRow);
1819                    selectedFileDetails.setStatus(null); // Clear any transient error.
1820                    // If there is a permanent error
1821                    // then remove without asking further
1822                    // else ask for confirmation.
1823                    if(selectedFileDetails.getStatus(logic) != null)
1824                        { logic.uploadingFiles.remove(selectedRow); }
1825                    else
1826                        {
1827                        // Double-check before zapping possibly-OK entry...
1828                        if(JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(pane, "Sure you want to stop trying to upload " + selectedFileDetails.getLocalFilename() + "?", fileClearButton.getText(), JOptionPane.OK_CANCEL_OPTION))
1829                            { logic.uploadingFiles.remove(selectedRow); }
1830                        }
1831                    queueTableModel.fireTableDataChanged();
1832                    }
1833                });
1834            fileClearButton.setToolTipText("Give up trying to upload this file.");
1835            pane.add(fileClearButton, c);
1836            ++c.gridy;
1837            c.gridx = 0;
1838            final JButton fileRetryButton = new JButton("Retry");
1839            fileRetryButton.setEnabled(false); // Initially false.
1840            fileRetryButton.addActionListener(new ActionListener()
1841                {
1842                /**Clear errors from the current selected row if the button is pressed. */
1843                public void actionPerformed(final ActionEvent e)
1844                    {
1845                    final int selectedRow = queueTable.getSelectedRow();
1846                    if(selectedRow == -1) { return; /* Nothing to do... */ }
1847                    logic.uploadingFiles.get(selectedRow).setStatus(null); // Clear any transient error.
1848                    queueTableModel.fireTableDataChanged();
1849                    }
1850                });
1851            fileRetryButton.setToolTipText("Try to clear any error on this file to retry upload.");
1852            pane.add(fileRetryButton, c);
1853            ++c.gridy;
1854            c.gridx = 0;
1855            final JButton fileRetryAllButton = new JButton("Retry All");
1856            fileRetryAllButton.setEnabled(false); // Initially false.
1857            fileRetryAllButton.addActionListener(new ActionListener()
1858                {
1859                /**Clear all errors if the button is pressed. */
1860                public void actionPerformed(final ActionEvent e)
1861                    {
1862                    synchronized(logic.uploadingFiles)
1863                        {
1864                        for(int i = logic.uploadingFiles.size(); --i >= 0; )
1865                            { logic.uploadingFiles.get(i).setStatus(null); } // Clear any transient errors.
1866                        }
1867                    queueTableModel.fireTableDataChanged();
1868                    }
1869                });
1870            fileRetryAllButton.setToolTipText("Try to clear errors on all files to retry upload.");
1871            pane.add(fileRetryAllButton, c);
1872    
1873    
1874    
1875    
1876    
1877            // The listener that will be polled regularly.
1878            final ActionListener al = new ActionListener()
1879                {
1880                public void actionPerformed(final ActionEvent e)
1881                    {
1882                    // Redraw the table if necessary.
1883                    if(logic.uploadingFiles.testAndClearChangedFlag())
1884                        {
1885                        // Get table redrawn.
1886                        queueTableModel.fireTableDataChanged();
1887    
1888                        // Update metrics/status.
1889                        uploadedCount.setText(String.valueOf(logic.getCompletedUploadsThisSession()));
1890                        final long smoothedUploadSpeedBps = logic.getSmoothedUploadSpeedBps();
1891                        uploadSpeedValue.setText((smoothedUploadSpeedBps <= 0) ? "?" : (TextUtils.sizeAsText(smoothedUploadSpeedBps, true) + "/s"));
1892    
1893                        if(logic.uploadingFiles.size() == 0)
1894                            {
1895                            uploadCompleteValue.setText(TEXT_NA);
1896                            }
1897                        else
1898                            {
1899                            // Conversative estimate of bytes left to transfer.
1900                            long bytesLeft = 0;
1901                            // Grab a long on the upload table
1902                            // so that it does not change under our feet...
1903                            synchronized(logic.uploadingFiles)
1904                                {
1905                                for(int i = logic.uploadingFiles.size(); --i >= 0; )
1906                                    {
1907                                    final SelectedFileDetails selectedFileDetails = logic.uploadingFiles.get(i);
1908                                    bytesLeft += selectedFileDetails.getEsa().length;
1909                                    }
1910                                }
1911                            // Guess 10kBytes/s transfer speed until first measurements are available.
1912                            final long timeEst = bytesLeft / ((smoothedUploadSpeedBps <= 0) ? 10000 : smoothedUploadSpeedBps);
1913                            uploadCompleteValue.setText(String.valueOf(timeEst) + "s");
1914                            }
1915                        }
1916    
1917                    // Only enable the buttons if the table is not empty!
1918                    final boolean tableIsNotEmpty = (logic.uploadingFiles.size() > 0);
1919                    fileClearButton.setEnabled(tableIsNotEmpty);
1920                    fileRetryButton.setEnabled(tableIsNotEmpty);
1921                    fileRetryAllButton.setEnabled(tableIsNotEmpty);
1922    
1923                    // Highlight items being uploaded.
1924                    for(int i = UploaderLogic.MAX_CONC_UPLOADS; --i >= 0; )
1925                        {
1926                        final UploadingNowItem uploadingNowItem = uploadingNow[i];
1927                        final UploaderLogic.UploadStatus status = logic.getInProgressSlot(i);
1928                        final JProgressBar progressBar = uploadingNowItem.progressBar;
1929                        if(status == null) // Not in use.
1930                            {
1931                            // Did anything change?
1932                            // Save lots of useless update CPU if not...
1933                            final String idleMsg = "idle";
1934                            if(!idleMsg.equals(progressBar.getString()))
1935                                {
1936                                progressBar.setString(idleMsg); // Hide % string.
1937                                uploadingNowItem.sizeBox.setText(null);
1938                                uploadingNowItem.nameBox.setText(null);
1939                                progressBar.setMinimum(0);
1940                                progressBar.setMaximum(0);
1941                                progressBar.setValue(0);
1942                                progressBar.setIndeterminate(false);
1943    
1944                                // Change main app title back to normal to show no uploads in progress...
1945                                UploaderMain.this.setTitle(APPLICATION_WINDOW_TITLE);
1946    
1947                                // Show any changes to the in-progress table...
1948                                queueTableModel.fireTableDataChanged();
1949    
1950                                // Log when all uploads have finished.
1951                                // This may log up to once per worker thread
1952                                // if all threads stop uploads very close together.
1953                                if((logic.uploadingFiles.size() == 0) && // None left to upload.
1954                                   (logic.getCompletedUploadsThisSession() > 0)) // Not at start-up!
1955                                    { logger.log("Uploads finished..."); }
1956                                }
1957                            }
1958                        else // In progress...
1959                            {
1960                            progressBar.setString(null); // Show % string.
1961                            final long fileLength = status.file.getEsa().length;
1962                            // Show (short) exhibit name and length.
1963                            final String newFileComponent = status.file.getEsa().getExhibitFullName().getShortName().toString();
1964                            if(!newFileComponent.equals(uploadingNowItem.nameBox.getText()))
1965                                {
1966                                // If name for this upload has changed...
1967                                uploadingNowItem.nameBox.setText(newFileComponent);
1968                                // then show any changes to the in-progress table...
1969                                queueTableModel.fireTableDataChanged();
1970                                }
1971                            uploadingNowItem.sizeBox.setText(String.valueOf(fileLength));
1972                            // Make an int-safe clamped version of fileLength.
1973                            final int clampedLen = (int) Math.min(fileLength, Integer.MAX_VALUE);
1974                            // Maximum is "safe" clamped length of exhibit.
1975                            progressBar.setMinimum(0); // Minimum is 0 bytes uploaded.
1976                            progressBar.setMaximum(clampedLen);
1977    
1978                            final long bytesUploaded = status.getBytesUploaded();
1979                            // Value is bytes-sent clamped into range...
1980                            final int newProgressValue = Math.max(0, (int) Math.min(clampedLen, bytesUploaded));
1981                            if(newProgressValue != progressBar.getValue())
1982                                {
1983                                // If progress for this upload has changed...
1984                                progressBar.setValue(newProgressValue);
1985                                // then show any changes to the in-progress table...
1986                                queueTableModel.fireTableDataChanged();
1987                                }
1988                            // Indeterminate iff an out-of-range value,
1989                            // used to indicate not started or waiting for response...
1990                            progressBar.setIndeterminate((bytesUploaded < 0) || (bytesUploaded > fileLength));
1991    
1992                            // Change main app title to show upload(s) in progress...
1993                            final StringBuilder sb = new StringBuilder(APPLICATION_WINDOW_TITLE.length() + 32);
1994                            sb.append(APPLICATION_WINDOW_TITLE);
1995                            sb.append(": UPLOADING...");
1996                            final boolean oddSecond = (((System.currentTimeMillis() / 1000) & 1) != 0);
1997                            // Flashing constant-width activity indicator!
1998                            sb.append(oddSecond ? " .*" : " *.");
1999                            final String title = sb.toString();
2000                            if(!title.equals(UploaderMain.this.getTitle()))
2001                                { UploaderMain.this.setTitle(title); }
2002                            }
2003                        }
2004                    }
2005                };
2006    
2007            return(new Tuple.Pair<JComponent, ActionListener>(pane, al));
2008            }
2009    
2010    
2011    
2012        /**Make the standard insets to wrap round most components.
2013         * Note that the returned Insets() object is mutable.
2014         */
2015        private static Insets makeStandardInsets()
2016            { return(new Insets(2, 2, 2, 2)); }
2017    
2018        /**Creates and initialises a status bar. */
2019        private JLabel createStatusBar()
2020            {
2021            final JLabel sbar = new JLabel("Ready");
2022            sbar.setBorder(BorderFactory.createEtchedBorder());
2023            return(sbar);
2024            }
2025    
2026        /**This method acts as the Action handler delegate for all the actions. */
2027        public void actionPerformed(final ActionEvent evt)
2028            {
2029            final String command = evt.getActionCommand();
2030    
2031            // Compare the action command to the known actions,
2032            // most-frequent first for efficiency.
2033            if(command.equals(selectAction.getActionCommand()))
2034                {
2035                // The "Select/Name for upload" action was invoked...
2036                uploadTableEditRow.set(-1); // Nothing in the table being edited now.
2037                selectFilesForUpload();
2038                }
2039            else if(command.equals(aboutAction.getActionCommand()))
2040                {
2041                // The "About" action was invoked...
2042                JOptionPane.showMessageDialog(this, aboutAction.getLongDescription(), aboutAction.getShortDescription(), JOptionPane.INFORMATION_MESSAGE);
2043                }
2044            else if(command.equals(exitAction.getActionCommand()))
2045                {
2046                // The "Exit" action was invoked...
2047                // Shut down unless vetoed...
2048                try { shutdown(); }
2049                catch(final UnsupportedOperationException e) { }
2050                }
2051            else
2052                {
2053                logger.log("Unexpected command: " + command);
2054                }
2055    
2056            // Note user activity...
2057            logic.setUserLastActive("actionPeformed: " + command);
2058            }
2059    
2060        /**This adapter is constructed to handle mouse-over component events. */
2061        private static final class MouseHandler extends MouseAdapter
2062            {
2063            private JLabel label;
2064            private String oldMsg;
2065    
2066            /**Adaptor constructor.
2067             * @param label the JLabel which will recieve value of the
2068             *              Action.LONG_DESCRIPTION key
2069             */
2070            public MouseHandler(final JLabel label)
2071                {
2072                setLabel(label);
2073                oldMsg = label.getText();
2074                }
2075    
2076            public void setLabel(final JLabel label)
2077                {
2078                this.label = label;
2079                }
2080    
2081            @Override
2082            public void mouseEntered(final MouseEvent evt)
2083                {
2084                if(evt.getSource() instanceof AbstractButton)
2085                    {
2086                    final AbstractButton button = (AbstractButton) evt.getSource();
2087                    final Action action = button.getAction(); // getAction is new in JDK 1.3
2088                    if(action != null)
2089                        {
2090                        oldMsg = label.getText();
2091                        final String message = (String) action.getValue(Action.LONG_DESCRIPTION);
2092                        label.setText(message);
2093                        }
2094                    }
2095                }
2096    
2097            @Override
2098            public void mouseExited(final MouseEvent evt)
2099                {
2100                label.setText(oldMsg);
2101                }
2102            }
2103    
2104        /**Select (in a file-chooser) some files for upload.
2105         * Since we will have to interact with disc, eg to compute hashes,
2106         * this may take a while for many and/or large files.
2107         */
2108        private void selectFilesForUpload()
2109            {
2110            // Selected files before we start...
2111            final int filesBefore = logic.selectedFiles.size();
2112    
2113            // Make sure initial name is unique if not already so.
2114            logic.uibIncNumberIfNotZeroWhileNotUnique(uib);
2115    
2116            // Putative complete name.
2117            final String putativeName = uib.getFullName();
2118    
2119            // Can only load a single file if number-in-series is 0.
2120            final boolean singleFile = (uib.getNumber() == 0);
2121    
2122            // We double-check the validity and newness/uniqueness of putativeName
2123            // to avoid most obvious race conditions...
2124            final boolean nameOK = ExhibitName.validNameSyntax(putativeName) &&
2125                (!logic.getAep().aeid.isPresent(putativeName)) &&
2126                (logic.getAep().aeid.getFullName(ExhibitName.getFileComponent(putativeName).toString()) == null);
2127    
2128            // Suggested extension(s).
2129            final String extensionComponent = ExhibitName.getExtensionComponent(putativeName).toString();
2130            final String extensions[] = { extensionComponent };
2131    
2132            // Try to open/check the suggested file(s).
2133            final FileContents[] files;
2134            try
2135                {
2136                if(!nameOK)
2137                    {
2138                    JOptionPane.showMessageDialog(this, "Cannot upload with name: " + putativeName, selectAction.getShortDescription(), JOptionPane.ERROR_MESSAGE);
2139                    return;
2140                    }
2141                else if(logic.fos == null)
2142                    {
2143                    if(JWS_UPLOAD_ONLY)
2144                        {
2145                        JOptionPane.showMessageDialog(this, "Cannot upload: no JWS FileOpenService", selectAction.getShortDescription(), JOptionPane.ERROR_MESSAGE);
2146                        return;
2147                        }
2148    
2149                    final JFileChooser chooser = new JFileChooser(logic.props.getFileChooserPathHint());
2150                    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
2151                    chooser.setMultiSelectionEnabled(!singleFile);
2152                    chooser.addChoosableFileFilter(new FileFilter()
2153                        {
2154                        /**Accept files with the correct extension(s). */
2155                        @Override
2156                        public final boolean accept(final File f)
2157                            {
2158                            // Allow directory traversal.
2159                            if(f.isDirectory()) { return(true); }
2160    
2161                            // Allow files with correct extension.
2162                            for(final String ext : extensions)
2163                                {
2164                                if(f.getPath().endsWith(ext) &&
2165                                   f.getPath().endsWith("." + ext))
2166                                    { return(true); }
2167                                }
2168                            return(false);
2169                            }
2170    
2171                        /**Get the description of each extension type accepted. */
2172                        @Override
2173                        public final String getDescription()
2174                            {
2175                            final StringBuilder sb = new StringBuilder();
2176                            for(final String ext : extensions)
2177                                {
2178                                if(sb.length() != 0) { sb.append(", "); }
2179                                final ExhibitMIME.ExhibitTypeParameters exhibitType = ExhibitMIME.getExhibitType(ext);
2180                                if(null != exhibitType)
2181                                    {
2182                                    final String description = exhibitType.description;
2183                                    if(description.length() > 0)
2184                                        {
2185                                        sb.append(description);
2186                                        sb.append(' ');
2187                                        }
2188                                    }
2189                                sb.append("*.").append(ext);
2190                                }
2191                            return(sb.toString());
2192                            }
2193                        });
2194                    if(JFileChooser.APPROVE_OPTION != chooser.showDialog(this, "Select"))
2195                        { files = new FileContents[0]; }
2196                    else
2197                        {
2198                        File[] selectedFiles = chooser.getSelectedFiles();
2199                        if(((selectedFiles == null) || (selectedFiles.length == 0)) &&
2200                           (chooser.getSelectedFile() != null))
2201                            { selectedFiles = new File[]{chooser.getSelectedFile()}; }
2202                        files = new FileContents[selectedFiles.length];
2203                        for(int i = selectedFiles.length; --i >= 0; )
2204                            {
2205                            final File f = selectedFiles[i];
2206                            assert(f != null);
2207                            files[i] = new NormalFileContent(f);
2208                            }
2209                        }
2210                    // Try to remember where we were...
2211                    logic.props.setFileChooserPathHint(chooser.getCurrentDirectory());
2212                    }
2213                else
2214                    {
2215                    if(singleFile)
2216                        {
2217                        final File dir = logic.props.getFileChooserPathHint();
2218                        final FileContents fileContents = logic.fos.openFileDialog(
2219                            (dir == null) ? null : dir.getPath(), extensions);
2220                        if(fileContents == null)
2221                            { files = new FileContents[0]; }
2222                        else
2223                            { files = new FileContents[]{fileContents}; }
2224                        }
2225                    else
2226                        {
2227                        files = logic.fos.openMultiFileDialog(null, extensions);
2228                        }
2229                    }
2230    
2231                // Use the directory (if any) of the last file chosen
2232                // to start the next search.
2233                for(int i = files.length; --i >= 0; )
2234                    {
2235                    if(files[i] == null) { continue; }
2236                    final File parentFile = (new File(files[i].getName())).getParentFile();
2237                    if(parentFile == null) { continue; }
2238                    logic.props.setFileChooserPathHint(parentFile);
2239                    break;
2240                    }
2241    
2242                if(files.length > 0)
2243                    {
2244                    final boolean autoMode = autoSuffixCheckBox.isSelected();
2245    
2246                    // THIS CALL may be slow if there are many/large files.
2247                    final String err = logic.addSelectedFiles(autoMode, files, uib); // FIXME
2248    
2249                    if(err != null)
2250                        {
2251                        JOptionPane.showMessageDialog(this, err, "File problem", JOptionPane.ERROR_MESSAGE);
2252                        return;
2253                        }
2254                    }
2255                }
2256            catch(final IOException e)
2257                {
2258                logger.log("Failed to open file(s) for upload: " + e.getMessage());
2259                JOptionPane.showMessageDialog(this, "Cannot upload: IOException: " + e.getMessage(), selectAction.getShortDescription(), JOptionPane.ERROR_MESSAGE);
2260                return;
2261                }
2262            finally
2263                {
2264                // Make sure that the table display is up-to-date...
2265                // ...and that we are not "editing" any row in the table.
2266                uploadTableEditRow.set(-1); // Nothing in the table being edited now.
2267                uploadTableModel.fireTableDataChanged();
2268                }
2269    
2270            final int filesAfter = logic.selectedFiles.size();
2271            JOptionPane.showMessageDialog(this, "Selected new files for upload: " + (filesAfter-filesBefore), selectAction.getShortDescription(), JOptionPane.INFORMATION_MESSAGE);
2272            }
2273    
2274    
2275    
2276    
2277        /**ActionListener invoked by pollUI; never null. */
2278        private final ActionListener pollAL;
2279    
2280        /**Poll UI (in Swing thread, ie Swing-safe). */
2281        private void pollUI(final ActionEvent e)
2282            {
2283            pollAL.actionPerformed(e);
2284            }
2285    
2286    
2287        /**Listener class used to veto attempts to start another app instance.
2288         * A (modal) dialog[ue] is shown instead.
2289         */
2290        private static final class SISListener implements SingleInstanceListener
2291            {
2292            public final void newActivation(final String[] params)
2293                {
2294                System.err.println("Attempted to launch another instance, args: " + Arrays.asList(params));
2295    
2296                // Schedule a job for the event-dispatching thread:
2297                // creating and showing a blocking dialogue.
2298                javax.swing.SwingUtilities.invokeLater(new Runnable(){
2299                    public final void run()
2300                        {
2301                        JOptionPane.showMessageDialog(null, "Uploader already running...", "Already running", JOptionPane.ERROR_MESSAGE);
2302                        }
2303                    });
2304                }
2305            }
2306    
2307        /**Main method invoked from JWS.
2308         */
2309        public static void main(final String[] args)
2310            {
2311            // Try to set a local look-and-feel...
2312            try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
2313            catch(final Exception e) { }
2314    
2315            // Schedule a job for the event-dispatching thread:
2316            // creating and showing this application's GUI.
2317            // Also start a daemon thread to drive async non-GUI activity.
2318            javax.swing.SwingUtilities.invokeLater(new Runnable(){
2319                public final void run()
2320                    {
2321                    final UploaderMain mainFrame = new UploaderMain();
2322                    mainFrame.pack();
2323                    mainFrame.setVisible(true);
2324    
2325                    // After displaying the UI,
2326                    // but before allowing much else to happen,
2327                    // reload any persisted data
2328                    // and start any background processing...
2329                    mainFrame.logic.startup();
2330    
2331                    // Start a (daemon) poller thread for UI async activity.
2332                    // This is called from a Swing timerUI, so is Swing-safe.
2333                    // This is created and started AFTER the UI object construction
2334                    // is complete to avoid any unpleasant races.
2335                    // We call this every 100ms or so in order to feel responsive.
2336                    final Timer timerUI = new javax.swing.Timer(70 + Rnd.fastRnd.nextInt(30),
2337                                                                (new ActionListener(){
2338                        /**Poll the UI logic. */
2339                        public void actionPerformed(final ActionEvent e)
2340                            { mainFrame.pollUI(e); }
2341                        }));
2342                    // Delay first poll a little to allow system to start up...
2343    //                timerUI.setInitialDelay(301);
2344                    timerUI.start();
2345                    }
2346                });
2347            }
2348    
2349        /**Unique Serialisation class ID generated by http://random&#46;hd&#46;org/. */
2350        private static final long serialVersionUID = -8885213711913005041L;
2351        }