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.atHome;
031
032 import java.awt.BorderLayout;
033 import java.awt.GridBagConstraints;
034 import java.awt.GridBagLayout;
035 import java.awt.Insets;
036 import java.awt.event.ActionEvent;
037 import java.awt.event.ActionListener;
038 import java.awt.event.MouseAdapter;
039 import java.awt.event.MouseEvent;
040 import java.awt.event.WindowAdapter;
041 import java.awt.event.WindowEvent;
042 import java.util.Arrays;
043
044 import javax.jnlp.SingleInstanceListener;
045 import javax.swing.AbstractButton;
046 import javax.swing.Action;
047 import javax.swing.BorderFactory;
048 import javax.swing.JComponent;
049 import javax.swing.JFrame;
050 import javax.swing.JLabel;
051 import javax.swing.JMenu;
052 import javax.swing.JMenuBar;
053 import javax.swing.JOptionPane;
054 import javax.swing.JPanel;
055 import javax.swing.JProgressBar;
056 import javax.swing.JSlider;
057 import javax.swing.Timer;
058 import javax.swing.UIManager;
059
060 import org.hd.d.pg2k.ai.scorer.MiniScorerCacheImpl;
061 import org.hd.d.pg2k.ai.scorer.ScoreAndConf;
062 import org.hd.d.pg2k.svrCore.Rnd;
063 import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
064 import org.hd.d.pg2k.svrCore.TextUtils;
065 import org.hd.d.pg2k.svrCore.Tuple;
066
067
068 /**Main (UI) class of JWS-based exhibit uploader.
069 * Runs as a Swing App using JNLP resource-access facilities
070 * such as persistence and file read/write.
071 * <p>
072 * This class/file contains as little non-UI code as is reasonably practical,
073 * so that if we change the UI details then other classes should be unaffected.
074 * <p>
075 * We don't really want/need this class to be Serializable,
076 * but this class inherits Serializable from JFrame.
077 */
078 public final class AHJWSMain extends JFrame implements ActionListener
079 {
080 /**Central logger instance for uploader; never null.
081 * This instance may log to the status bar and elsewhere.
082 */
083 private final SimpleLoggerIF logger = new SimpleLoggerIF()
084 {
085 public final void log(final String message)
086 {
087 // Log to the Java console.
088 System.out.println(message);
089
090 // Schedule a job for the event-dispatching thread:
091 // logging this message in the status bar.
092 // We hope that these events do not get re-ordered too often.
093 javax.swing.SwingUtilities.invokeLater(new Runnable(){
094 public final void run()
095 { status.setText(message); }
096 });
097 }
098 };
099
100 /**Our companion "business-logic" class; never null. */
101 private final AHStandaloneMain logic;
102
103 /**The action performed by the "About" menu entry. */
104 private final AboutAction aboutAction = new AboutAction();
105 /**The action performed by the "Exit" menu entry. */
106 private final ExitAction exitAction = new ExitAction();
107
108 /**Status bar; never null. */
109 private final JLabel status = createStatusBar();
110
111 /**Single listener instance. */
112 private final SISListener sisListener = new SISListener();
113
114 /**Handles Mouse over messages on toolbar buttons and menu items; never null. */
115 private final MouseHandler mouseHandler = new MouseHandler(status);
116
117 /**Title shown for application. */
118 private static final String APPLICATION_WINDOW_TITLE = "Gallery 'At Home' Worker";
119
120
121 /**Create an instance of the Worker app main window.
122 * Designed to be called by main().
123 */
124 private AHJWSMain()
125 {
126 super(APPLICATION_WINDOW_TITLE);
127
128 // Set up user actions.
129 initActions();
130
131 // Create the logic/worker.
132 logic = new AHStandaloneMain(logger);
133
134 // ASAP, try to avoid multiple instances being started.
135 if(logic.sis != null)
136 { logic.sis.addSingleInstanceListener(sisListener); }
137
138 // Create the contents of this frame...
139 setJMenuBar(createMenu());
140 final Tuple.Pair<JComponent, ActionListener> mainPane = createMainPane();
141 pollAL = mainPane.second;
142 assert(mainPane != null);
143 assert(mainPane.first != null);
144 assert(mainPane.second != null);
145 getContentPane().add(mainPane.first, BorderLayout.CENTER);
146 getContentPane().add(status, BorderLayout.SOUTH);
147
148 // Arrange to exit the VM if the window is closed...
149 addWindowListener(new WindowAdapter()
150 {
151 @Override
152 public final void windowClosing(final WindowEvent evt)
153 {
154 // Shut down gracefully (and exit()) if allowed to,
155 // else attempt to veto with an exception.
156 try { shutdown(); }
157 catch(final UnsupportedOperationException e) { }
158 }
159 });
160 }
161
162 /**Perform any activity required to shut down cleanly, eg save state, then exit.
163 * This should try to avoid taking a long time.
164 * <p>
165 * This may throw an UnsupportedOperationException to try to veto
166 * an exit that the user changes their mind about,
167 * eg because we have work in progress!
168 *
169 * @throws UnsupportedOperationException if the user vetoes the shut-down
170 */
171 private void shutdown()
172 throws UnsupportedOperationException
173 {
174 // Do any shutdown required by the non-GUI components...
175 try { logic.shutdown(); }
176 catch(final Throwable t) { /* Absorb: we're trying to quit ASAP... */ }
177
178 // Now allow other instances to be started...
179 if(logic.sis != null)
180 { logic.sis.removeSingleInstanceListener(sisListener); }
181
182 // Now allow a gracefull exit!
183 try { System.exit(0); }
184 catch(final Throwable t) { /* Absorb: we're trying to quit ASAP... */ }
185 }
186
187 /**This method should be called before creating the UI to create all the Actions. */
188 private void initActions()
189 {
190 // actions.clear();
191 registerAction(aboutAction);
192 registerAction(exitAction);
193 }
194
195 private void registerAction(final JLFAbstractAction action)
196 {
197 action.addActionListener(this);
198 // actions.addElement(action);
199 }
200
201 /**Create the application menu bar. */
202 private JMenuBar createMenu()
203 {
204 final JMenuBar menuBar = new JMenuBar();
205
206 // Build the File menu
207 final JMenu fileMenu = new JMenu("File");
208 fileMenu.setMnemonic('F');
209 fileMenu.add(exitAction).addMouseListener(mouseHandler);
210
211 // Build the help menu
212 final JMenu helpMenu = new JMenu("Help");
213 helpMenu.setMnemonic('H');
214 helpMenu.add(aboutAction).addMouseListener(mouseHandler);
215
216 menuBar.add(fileMenu);
217 menuBar.add(helpMenu);
218
219 return menuBar;
220 }
221
222 /**Create the (main) tabbed pane component and a lister for UI polling. */
223 private Tuple.Pair<JComponent, ActionListener> createMainPane()
224 {
225 // Have a lone simple status panel for now.
226 return(createStatusPanel());
227 }
228
229 /**Make the login/status component; never null.
230 * Also creates a listener suitable to poll regularly
231 * from the Swing thread to do async UI updates.
232 */
233 private Tuple.Pair<JComponent, ActionListener> createStatusPanel()
234 {
235 final JPanel pane = new JPanel(new GridBagLayout());
236 final GridBagConstraints c = new GridBagConstraints();
237
238 // Wrap insets round everything.
239 c.insets = makeStandardInsets();
240
241 // Heading top (left).
242 c.gridy = 0;
243 c.gridx = 0;
244 c.gridwidth = GridBagConstraints.REMAINDER; // Span all columns.
245 final JLabel heading = new JLabel("<html><strong>STATUS</strong></html>");
246 pane.add(heading, c);
247 c.gridwidth = 1; // Back to default.
248
249 // New/best Scorers reported home.
250 c.gridx = 0;
251 ++c.gridy;
252 final JLabel genLabel = new JLabel("Generation:");
253 pane.add(genLabel, c);
254 ++c.gridx;
255 final JLabel gen = new JLabel("????"); // Pad to roughly appropriate size.
256 pane.add(gen, c);
257 genLabel.setLabelFor(gen);
258
259 // New/best Scorers reported home.
260 c.gridx = 0;
261 ++c.gridy;
262 final JLabel scReportedLabel = new JLabel("New/best Scorers found/reported:");
263 pane.add(scReportedLabel, c);
264 ++c.gridx;
265 final JLabel scReported = new JLabel("??????"); // Pad to roughly appropriate size.
266 pane.add(scReported, c);
267 scReportedLabel.setLabelFor(scReported);
268
269 // Rating of last/best Scorer found as % of best possible...
270 c.gridx = 0;
271 ++c.gridy;
272 final JLabel scLastPCLabel = new JLabel("Last reported Scorer's rating:");
273 pane.add(scLastPCLabel, c);
274 ++c.gridx;
275 final JProgressBar scLastPC = new JProgressBar(0, 100);
276 scLastPC.setStringPainted(true); // Show a string.
277 scLastPC.setString("-"); // Hide % string.
278 scLastPC.setIndeterminate(false);
279 pane.add(scLastPC, c);
280 scLastPCLabel.setLabelFor(scLastPC);
281
282 // Current population size.
283 c.gridx = 0;
284 ++c.gridy;
285 final JLabel popSizeLabel = new JLabel("Population size:");
286 pane.add(popSizeLabel, c);
287 ++c.gridx;
288 final JProgressBar popSize = new JProgressBar(0, MiniScorerCacheImpl.MAX_MINI_SCORER_SCORES_RETAINED);
289 popSize.setStringPainted(true); // Show a string.
290 popSize.setString("0"); // Hide % string.
291 popSize.setIndeterminate(false);
292 pane.add(popSize, c);
293 popSizeLabel.setLabelFor(popSize);
294
295 // Time remaining for this generation.
296 c.gridx = 0;
297 ++c.gridy;
298 final JLabel gTRLabel = new JLabel("Generation time remaining:");
299 pane.add(gTRLabel, c);
300 ++c.gridx;
301 final JProgressBar gTR = new JProgressBar(0, AHStandaloneMain.MIN_GENERATION_MS);
302 gTR.setStringPainted(true); // Show a string.
303 gTR.setString("-"); // Hide % string.
304 gTR.setIndeterminate(false);
305 pane.add(gTR, c);
306 gTRLabel.setLabelFor(gTR);
307
308 // Calibration set size...
309 c.gridx = 0;
310 ++c.gridy;
311 final JLabel calibSizeLabel = new JLabel("Calibration set size:");
312 pane.add(calibSizeLabel, c);
313 ++c.gridx;
314 final JLabel calibSize = new JLabel("???"); // Pad to roughly appropriate size.
315 pane.add(calibSize, c);
316 calibSizeLabel.setLabelFor(calibSize);
317
318 // Memory usage and total available...
319 c.gridx = 0;
320 ++c.gridy;
321 final JLabel memoryUsageLabel = new JLabel("Memory use/total:");
322 pane.add(memoryUsageLabel, c);
323 ++c.gridx;
324 final JLabel memoryUsage = new JLabel("???MB / ???MB"); // Pad to roughly appropriate size.
325 pane.add(memoryUsage, c);
326 memoryUsageLabel.setLabelFor(memoryUsage);
327
328 // Target CPU load...
329 c.gridx = 0;
330 ++c.gridy;
331 final JLabel pcCPULabel = new JLabel("CPU target load %:");
332 pane.add(pcCPULabel, c);
333 ++c.gridx;
334 // We allow the bounds to go down to 0 for simplicity.
335 final JSlider pcCPU = new JSlider(0, 100, AHStandaloneMain.DEFAULT_CPU_PERCENT);
336 pcCPU.setMajorTickSpacing(20);
337 pcCPU.setMinorTickSpacing(5);
338 pcCPU.setPaintLabels(true);
339 pcCPU.setPaintTicks(true);
340 pane.add(pcCPU, c);
341 pcCPULabel.setLabelFor(pcCPU);
342
343 // Create a listener to handle async UI-safe updates.
344 final ActionListener al = new ActionListener(){
345 public void actionPerformed(final ActionEvent evt)
346 {
347 // try
348 // {
349 // }
350 // catch(final Exception e)
351 // {
352 // e.printStackTrace();
353 //// serverConnectionStatus.setText("unexpected exception: " + e.getMessage() + ((e.getStackTrace().length > 0) ? (" @ " + e.getStackTrace()[0]) : ""));
354 // }
355
356 // Update population stats.
357 gen.setText(Integer.toString(logic.getGeneration()));
358 final int populationSize = logic.getPopulationSize();
359 popSize.setValue(populationSize);
360 popSize.setString(Integer.toString(populationSize));
361 scReported.setText(Integer.toString(logic.getScorersReported()));
362 final int pcRating = (int) ((ScoreAndConf.computeScorerGoodness(logic.getLastSAC())*100L)/
363 (ScoreAndConf.MAX*ScoreAndConf.MAX));
364 scLastPC.setValue(pcRating);
365 scLastPC.setString(Integer.toString(pcRating) + '%');
366 final int genTimeRemaining = logic.getGenTimeRemaining();
367 gTR.setValue(genTimeRemaining);
368 // Show remaining time in minutes...
369 final int minsRemaining = ((genTimeRemaining + 30000) / 60000);
370 gTR.setString(Integer.toString(minsRemaining) + 'm');
371
372 // Update calibration set stats...
373 final int calibSetSize = logic.getCalibrationSetSize();
374 calibSize.setText(String.valueOf(calibSetSize));
375
376 // Update memory stats.
377 final Runtime r = Runtime.getRuntime();
378 memoryUsage.setText(TextUtils.sizeAsText(r.totalMemory() - r.freeMemory(), true) + " / " +
379 TextUtils.sizeAsText(r.totalMemory(), true));
380
381 // Update and track CPU usage.
382 // We set the target to the user-supplied value,
383 // but reflect any constraint on the value back to the UI.
384 logic.setTargetPercentCPU(pcCPU.getValue());
385 pcCPU.setValue(logic.getTargetPercentCPU());
386 }
387 };
388
389 return(new Tuple.Pair<JComponent, ActionListener>(pane, al));
390 }
391
392
393 /**Make the standard insets to wrap round most components.
394 * Note that the returned Insets() object is mutable.
395 */
396 private static Insets makeStandardInsets()
397 { return(new Insets(2, 2, 2, 2)); }
398
399 /**Creates and initialises a status bar. */
400 private JLabel createStatusBar()
401 {
402 final JLabel sbar = new JLabel("Ready");
403 sbar.setBorder(BorderFactory.createEtchedBorder());
404 return(sbar);
405 }
406
407 /**This method acts as the Action handler delegate for all the actions. */
408 public void actionPerformed(final ActionEvent evt)
409 {
410 final String command = evt.getActionCommand();
411
412 // Compare the action command to the known actions,
413 // most-frequent first for efficiency.
414 if(command.equals(aboutAction.getActionCommand()))
415 {
416 // The "About" action was invoked...
417 JOptionPane.showMessageDialog(this, aboutAction.getLongDescription(), aboutAction.getShortDescription(), JOptionPane.INFORMATION_MESSAGE);
418 }
419 else if(command.equals(exitAction.getActionCommand()))
420 {
421 // The "Exit" action was invoked...
422 // Shut down unless vetoed...
423 try { shutdown(); }
424 catch(final UnsupportedOperationException e) { }
425 }
426 else
427 {
428 logger.log("Unexpected command: " + command);
429 }
430 }
431
432 /**This adapter is constructed to handle mouse-over component events. */
433 private static final class MouseHandler extends MouseAdapter
434 {
435 private JLabel label;
436 private String oldMsg;
437
438 /**Adaptor constructor.
439 * @param label the JLabel which will recieve value of the
440 * Action.LONG_DESCRIPTION key
441 */
442 public MouseHandler(final JLabel label)
443 {
444 setLabel(label);
445 oldMsg = label.getText();
446 }
447
448 public void setLabel(final JLabel label)
449 {
450 this.label = label;
451 }
452
453 @Override
454 public void mouseEntered(final MouseEvent evt)
455 {
456 if(evt.getSource() instanceof AbstractButton)
457 {
458 final AbstractButton button = (AbstractButton) evt.getSource();
459 final Action action = button.getAction(); // getAction is new in JDK 1.3
460 if(action != null)
461 {
462 oldMsg = label.getText();
463 final String message = (String) action.getValue(Action.LONG_DESCRIPTION);
464 label.setText(message);
465 }
466 }
467 }
468
469 @Override
470 public void mouseExited(final MouseEvent evt)
471 {
472 label.setText(oldMsg);
473 }
474 }
475
476
477
478
479 /**ActionListener invoked by pollUI; never null. */
480 private final ActionListener pollAL;
481
482 /**Poll UI (in Swing thread, ie Swing-safe). */
483 private void pollUI(final ActionEvent e)
484 {
485 pollAL.actionPerformed(e);
486 }
487
488
489 /**Listener class used to veto attempts to start another app instance.
490 * A (modal) dialog[ue] is shown instead.
491 */
492 private static final class SISListener implements SingleInstanceListener
493 {
494 public final void newActivation(final String[] params)
495 {
496 System.err.println("Attempted to launch another instance, args: " + Arrays.asList(params));
497
498 // Schedule a job for the event-dispatching thread:
499 // creating and showing a blocking dialogue.
500 javax.swing.SwingUtilities.invokeLater(new Runnable(){
501 public final void run()
502 {
503 JOptionPane.showMessageDialog(null, "Gallery AH Worker already running...", "Already running", JOptionPane.ERROR_MESSAGE);
504 }
505 });
506 }
507 }
508
509 /**Main method invoked from JWS/JNLP.
510 * We don't use any arguments passed to us.
511 */
512 public static void main(final String[] args)
513 {
514 // Try to set a local look-and-feel...
515 try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
516 catch(final Exception e) { }
517
518 // Schedule a job for the event-dispatching thread:
519 // creating and showing this application's GUI.
520 // Also start a daemon thread to drive async non-GUI activity.
521 javax.swing.SwingUtilities.invokeLater(new Runnable(){
522 public final void run()
523 {
524 final AHJWSMain mainFrame = new AHJWSMain();
525 mainFrame.pack();
526 mainFrame.setVisible(true);
527
528 // // After displaying the UI,
529 // // but before allowing much else to happen,
530 // // reload any persisted data
531 // // and start any background processing...
532 // mainFrame.logic.startup();
533
534 // Start a (daemon) poller thread for UI async activity.
535 // This is called from a Swing timerUI, so is Swing-safe.
536 // This is created and started AFTER the UI object construction
537 // is complete to avoid any unpleasant races.
538 // We call this every 100ms or so in order to feel responsive.
539 final Timer timerUI = new javax.swing.Timer(70 + Rnd.fastRnd.nextInt(30),
540 (new ActionListener(){
541 /**Poll the UI logic. */
542 public void actionPerformed(final ActionEvent e)
543 { mainFrame.pollUI(e); }
544 }));
545 // Delay first poll a little to allow system to start up...
546 // timerUI.setInitialDelay(301);
547 timerUI.start();
548
549 // Run the worker, cast adrift in its own thread.
550 (new Thread("worker main thread") {
551 @Override public void run() { mainFrame.logic.doWork(); }
552 }).start();
553 }
554 });
555 }
556
557 /**Unique Serialisation class ID generated by http://random.hd.org/. */
558 private static final long serialVersionUID = -8895213711913005041L;
559 }