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