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