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.hd.org/. */
2351 private static final long serialVersionUID = -8885213711913005041L;
2352 }