Source code: org/altara/mars/swingui/MarsView.java
1 /* MARS Network Monitor Swing User Interface
2 Copyright (C) 1999 Brian H. Trammell
3 Copyright (C) 2002 Leapfrog Research & Development, LLC
4
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, it is available at
17 http:///www.gnu.org/copyleft/gpl.html, or by writing to the
18 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19 Boston, MA 02111-1307, USA.
20 */
21
22 package org.altara.mars.swingui;
23
24 import org.altara.util.*;
25 import org.altara.mars.*;
26 import org.altara.mars.engine.*;
27 import org.jdom.*;
28 import java.io.*;
29 import java.net.*;
30 import java.util.*;
31 import java.awt.*;
32 import java.awt.event.*;
33 import javax.swing.*;
34 import javax.swing.tree.*;
35 import javax.swing.event.*;
36 import javax.swing.border.*;
37
38 /** MarsView is the root of the MARS Swing user interface.
39 */
40
41 public class MarsView extends JFrame implements StatusView {
42
43 // Toolbar actions
44 private Action newAction;
45 private Action openAction;
46 private Action saveAction;
47 private Action startAction;
48 private Action stopAction;
49 private boolean lastSaveActionState;
50
51 // List and tree models
52 private FaultListModel flm;
53 private DefaultTreeModel stm;
54 private ServiceTreeChangeAdapter stca;
55 private DetailListModel dlm;
56
57 // Panels
58 private ChangeListPanel clp;
59 private ConfigPanel cfgp;
60
61 // Status bar
62 private Queue statusQ;
63 private JTextField statusbar;
64 private javax.swing.Timer statusQTimer;
65
66 private static final int SB_THROTTLE = 2;
67 private static final int SB_DELAY = 2000;
68
69 // Context menu support classes
70 private ServiceTreeContextMenuSupport stcms;
71 private FaultListContextMenuSupport flcms;
72
73 // Lists and trees
74 private JTree serviceTree;
75
76 // Save/Open files
77 private File curDir;
78 private File curSaveFile;
79
80 public MarsView () {
81 // set the window's title
82 super("Mars "+Main.VERSION);
83
84 // Get the controller (understood to be initialized
85 // and to have a bound MarsModel) - we'll use it a lot
86 // below
87 final Controller controller = Main.getMain().getController();
88 MarsModel model = controller.getModel();
89
90 // initialize toolbar (required for stca connection)
91 JToolBar toolbar = initializeToolbar();
92
93 // set file defaults
94 curDir = new File(".");
95 curSaveFile = null;
96
97 // create a fault list and bind it to the controller
98 flm = new FaultListModel(this);
99 controller.addStatusChangeListener(
100 new StatusChangeThreadAdapter(flm));
101 final JList faultList = new JList(flm);
102 JScrollPane faultListSP = new JScrollPane(faultList);
103 // set up various list options
104 faultListSP.setBorder(new TitledBorder("Current Faults"));
105 // set up the renderer for the fault list
106 FaultListRenderer flr = new FaultListRenderer(flm);
107 faultList.setCellRenderer(flr);
108 // make it a change listener
109 model.addMarsModelListener(flm);
110
111 // add context menus to the fault list
112 flcms = new FaultListContextMenuSupport(faultList);
113
114 // create a change log panel
115 clp = new ChangeListPanel(controller);
116
117 // create a service tree bound to the model
118 stm = new DefaultTreeModel(controller.getModel());
119 serviceTree = new JTree(stm);
120 JScrollPane serviceTreeSP = new JScrollPane(serviceTree);
121 // set up various tree options
122 serviceTree.setRootVisible(false);
123 serviceTreeSP.setBorder(new TitledBorder("Hosts & Services"));
124 // add a renderer
125 ServiceTreeRenderer str = new ServiceTreeRenderer();
126 serviceTree.setCellRenderer(str);
127 // use a ServiceTreeChangeAdapter to route change events to the
128 // correct places...
129 stca = new ServiceTreeChangeAdapter(controller,model,serviceTree,this);
130
131 // add context menus to the service tree
132 stcms = new ServiceTreeContextMenuSupport(this,serviceTree);
133
134 // bind the service tree selection to the fault list
135 faultList.addListSelectionListener(new ListSelectionListener() {
136 public void valueChanged(ListSelectionEvent lse) {
137 if (lse.getValueIsAdjusting()) return;
138 Object selObj = faultList.getSelectedValue();
139 if (!(selObj instanceof Service)) return;
140 TreePath selPath = ((Service)selObj).getTreePath();
141 serviceTree.setSelectionPath(selPath);
142 serviceTree.makeVisible(selPath);
143 }
144 });
145
146 // create a detail view bound to the controller
147 // and the service tree
148 dlm = new DetailListModel();
149 controller.addProbeListener(new ProbeThreadAdapter(dlm));
150 serviceTree.addTreeSelectionListener(dlm);
151 JList detailList = new JList(dlm);
152 JScrollPane detailListSP = new JScrollPane(detailList);
153 // set up various list options
154 detailListSP.setBorder(new TitledBorder("Service Detail"));
155 // no renderer as of yet - set font to size 10, though.
156 detailList.setFont(MarsAbstractRenderer.cellFont);
157
158 // create a configuration panel
159 cfgp = new ConfigPanel(this);
160
161 // add a status change listener to modify the titlebar
162 controller.addStatusChangeListener(new StatusChangeThreadAdapter(
163 new StatusChangeListener() {
164 public void statusChanged(StatusChangeEvent sce) {
165 updateTitleBar();
166 }
167 }));
168
169 // ensure the controller is stopped and set appropriate
170 // default enable states
171 controller.stop();
172 stopAction.setEnabled(false);
173 saveAction.setEnabled(false);
174 stcms.setEditActionsEnabled(true);
175
176 // stuff it all together into a tab pane
177 JSplitPane rightPane = new JSplitPane(
178 JSplitPane.VERTICAL_SPLIT, faultListSP, detailListSP);
179 JSplitPane frontPane = new JSplitPane(
180 JSplitPane.HORIZONTAL_SPLIT, serviceTreeSP, rightPane);
181 JTabbedPane tabPane = new JTabbedPane(JTabbedPane.BOTTOM);
182 tabPane.addTab("Current",frontPane);
183 tabPane.addTab("History",clp);
184 tabPane.addTab("Config",cfgp);
185
186
187 // create the status bar
188 statusQ = new Queue();
189 statusbar = new JTextField("Starting...");
190 statusbar.setEditable(false);
191
192 controller.addProbeListener(new ProbeListener() {
193 public void probeRun(ProbeEvent ev) {
194 showStatus("Checked "+ev.getService());
195 }
196 });
197
198 statusQTimer =
199 new javax.swing.Timer(SB_DELAY,new ActionListener() {
200 public void actionPerformed(ActionEvent ae) {
201 displayNextStatus();
202 }
203 });
204 statusQTimer.setCoalesce(true);
205 statusQTimer.setRepeats(true);
206 statusQTimer.start();
207
208 // now put it into the window
209 getContentPane().setLayout(new BorderLayout());
210 getContentPane().add(toolbar,BorderLayout.NORTH);
211 getContentPane().add(tabPane,BorderLayout.CENTER);
212 getContentPane().add(statusbar,BorderLayout.SOUTH);
213
214 // set up the window close listener
215 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
216 addWindowListener(new WindowAdapter() {
217 public void windowClosing(WindowEvent we) {
218 // for now, do this all in-loop
219 // later, we'll want to ask the app master
220 // to shut down for us
221 if (confirmDiscardChanges()) {
222 setVisible(false);
223 controller.stop();
224 System.exit(0);
225 }
226 }
227 });
228
229 // rock and roll
230 this.setSize(500,400);
231 this.show();
232 rightPane.setDividerLocation(0.5);
233 frontPane.setDividerLocation(0.4);
234 }
235
236 public void showStatus(String status) {
237 statusQ.enqueue(status);
238 }
239
240 private void displayNextStatus() {
241 String nextStatus = (String)statusQ.nb_dequeue();
242 if (nextStatus == null) {
243 if (Main.getMain().getController().isActive()) {
244 statusbar.setText("Monitoring.");
245 } else {
246 statusbar.setText("Idle.");
247 }
248 } else {
249 // check throttle
250 int backlog = statusQ.size();
251 if (backlog > SB_THROTTLE) {
252 statusQTimer.setDelay((SB_THROTTLE * SB_DELAY)/backlog);
253 }
254 // display next status
255 statusbar.setText(nextStatus);
256 }
257 }
258
259 void updateTitleBar() {
260 if (!Main.getMain().getController().isActive())
261 setTitle("Mars "+Main.VERSION);
262 else if (flm.getFaultCount() == 0) setTitle("Mars [No Faults]");
263 else if (flm.getFaultCount() == 1) setTitle("Mars [1 Fault]");
264 else setTitle("Mars ["+flm.getFaultCount()+" Faults]");
265 }
266
267 private JToolBar initializeToolbar() {
268 // create the toolbar
269 JToolBar toolbar = new JToolBar();
270
271 newAction =
272 new AbstractAction("New...",
273 IconService.getIcon("mi2_tbnew.gif")) {
274 public void actionPerformed(ActionEvent ae) {
275 newModel();
276 }
277 };
278
279 openAction =
280 new AbstractAction("Open...",
281 IconService.getIcon("mi2_tbopen.gif")) {
282 public void actionPerformed(ActionEvent ae) {
283 openModel();
284 }
285 };
286
287 saveAction =
288 new AbstractAction("Save...",
289 IconService.getIcon("mi2_tbsave.gif")) {
290 public void actionPerformed(ActionEvent ae) {
291 saveModel();
292 }
293 };
294
295 startAction =
296 new AbstractAction("Start",
297 IconService.getIcon("mi2_tbrun.gif")) {
298 public void actionPerformed(ActionEvent ae) {
299 startController();
300 }
301 };
302
303 stopAction =
304 new AbstractAction("Stop",
305 IconService.getIcon("mi2_tbstop.gif")) {
306 public void actionPerformed(ActionEvent ae) {
307 stopController();
308 }
309 };
310
311 // add actions to the toolbar
312 int i = 0;
313 toolbar.add(newAction).setToolTipText("New Config");
314 toolbar.add(openAction).setToolTipText("Open Config File...");
315 toolbar.add(saveAction).setToolTipText("Save Config File...");
316 toolbar.addSeparator();
317 toolbar.add(startAction).setToolTipText("Start Monitoring");
318 toolbar.add(stopAction).setToolTipText("Stop Monitoring");
319
320 // add toolbar keybindings to MarsView window
321 KeyActionSupport kas = new KeyActionSupport();
322 this.addKeyListener(kas);
323
324 kas.addCommandKey(KeyEvent.VK_N,newAction);
325 kas.addCommandKey(KeyEvent.VK_O,openAction);
326 kas.addCommandKey(KeyEvent.VK_S,saveAction);
327 kas.addCommandKey(KeyEvent.VK_B,startAction);
328 kas.addCommandKey(KeyEvent.VK_E,stopAction);
329
330 return toolbar;
331 }
332
333 private void newModel() {
334 // don't chuck current model without giving
335 // the user a chance to save
336 if (!confirmDiscardChanges()) return;
337
338 // disconnect the old model from the UI
339 Controller controller = Main.getMain().getController();
340 MarsModel oldModel = controller.getModel();
341 // FIXME: clearing listeners in discarded models should be the
342 // job of Main, or the controller.
343 oldModel.clearMarsModelListeners();
344
345 // create the new model
346 Main.getMain().newModel();
347 // ...and connect it to the UI
348 MarsModel newModel = controller.getModel();
349 stca.setModel(newModel);
350 stm.setRoot(newModel);
351 newModel.addMarsModelListener(flm);
352
353 // disable the save button
354 markUnsavedChanges(false);
355 }
356
357 private void openModel() {
358 // don't chuck current model without giving
359 // the user a chance to save
360 if (!confirmDiscardChanges()) return;
361
362 // get the file to open
363 File inFile = selectFileOpen();
364 // make sure a file was selected
365 if (inFile == null) return;
366
367 try {
368 // get the old model so we can disconnect it
369 MarsModel oldModel = Main.getMain().getController().getModel();
370 // try to load the new model
371 Main.getMain().loadConfig(inFile);
372 // that seemed to work. disconnect the old model
373 // FIXME: clearing listeners in discarded models should be the
374 // job of Main, or the controller.
375 oldModel.clearMarsModelListeners();
376 // and connect the new one
377 MarsModel newModel = Main.getMain().getController().getModel();
378 stca.setModel(newModel);
379 stm.setRoot(newModel);
380 newModel.addMarsModelListener(flm);
381 // disable the save button
382 markUnsavedChanges(false);
383 } catch (JDOMException ex) {
384 // report load exception to the user.
385 JOptionPane.showMessageDialog(this,
386 inFile.getName()+" could not be opened.\n"+
387 "The file is not a valid XML file.",
388 "File Open Error",JOptionPane.ERROR_MESSAGE);
389 ex.printStackTrace();
390 } catch (InvalidDocumentException ex) {
391 // report load exception to the user.
392 JOptionPane.showMessageDialog(this,
393 inFile.getName()+" could not be opened.\n"+
394 "The file contains the following error: "+ex.getMessage(),
395 "File Open Error",JOptionPane.ERROR_MESSAGE);
396 ex.printStackTrace();
397 } catch (UnknownHostException ex) {
398 // report load exception to the user.
399 JOptionPane.showMessageDialog(this,
400 inFile.getName()+" could not be opened.\n"+
401 "The host address "+ex.getMessage()+" could not be resolved.\n"+
402 "This may indicate a problem with your domain name servers.",
403 "File Open Error",JOptionPane.ERROR_MESSAGE);
404 ex.printStackTrace();
405 } catch (Exception ex) {
406 // report catch-all load exception to the user
407 JOptionPane.showMessageDialog(this,
408 inFile.getName()+" could not be opened.\n"+
409 "A plugin could not be properly configured\n"+
410 "because an unknown error occurred:\n"+ex.getMessage(),
411 "File Open Error",JOptionPane.ERROR_MESSAGE);
412 ex.printStackTrace();
413 }
414 }
415
416 private boolean saveModel() {
417 // select a file to save to, default to last save
418 File outFile = selectFileSave();
419 if (outFile == null) return false;
420
421 try {
422 // write the config out
423 Main.getMain().saveConfig(outFile);
424 // disable the save button
425 markUnsavedChanges(false);
426 // signal success
427 return true;
428 } catch (Exception ex) {
429 JOptionPane.showMessageDialog(this,
430 outFile.getName()+" could not be saved.\n"+
431 "You may not have permission to write the file, or the file\n"+
432 "may be open. Try saving to a different file or directory.",
433 "File Save Error",JOptionPane.ERROR_MESSAGE);
434 ex.printStackTrace();
435 return false;
436 }
437 }
438
439 private void startController() {
440 startAction.setEnabled(false);
441 stopAction.setEnabled(true);
442 newAction.setEnabled(false);
443 openAction.setEnabled(false);
444 lastSaveActionState = saveAction.isEnabled();
445 saveAction.setEnabled(false);
446 stcms.setEditActionsEnabled(false);
447 Main.getMain().getController().start();
448 // now do necessary UI updates
449 stca.repaintServiceNodes();
450 flm.updateListCache();
451 updateTitleBar();
452 }
453
454 private void stopController() {
455 stopAction.setEnabled(false);
456 startAction.setEnabled(true);
457 newAction.setEnabled(true);
458 openAction.setEnabled(true);
459 saveAction.setEnabled(lastSaveActionState);
460 stcms.setEditActionsEnabled(true);
461 Main.getMain().getController().stop();
462 // now do necessary UI updates
463 stca.repaintServiceNodes();
464 flm.updateListCache();
465 updateTitleBar();
466 }
467
468 void markUnsavedChanges(boolean changed) {
469 if (Main.getMain().getController().isActive()) {
470 lastSaveActionState = changed;
471 } else {
472 saveAction.setEnabled(changed);
473 }
474 }
475
476 boolean hasUnsavedChanges() {
477 if (Main.getMain().getController().isActive()) {
478 return lastSaveActionState;
479 } else {
480 return saveAction.isEnabled();
481 }
482 }
483
484 private boolean confirmDiscardChanges() {
485 // if the model hasn't changed, assume tacit approval
486 if (!hasUnsavedChanges()) return true;
487
488 // otherwise, ask the user
489 String changeMsg = "There are unsaved changes to the configuration.\n"+
490 "Do you wish to save them before proceeding?";
491 int confirmed = JOptionPane.showConfirmDialog(this,changeMsg,
492 "Unsaved Changes",JOptionPane.YES_NO_CANCEL_OPTION,
493 JOptionPane.WARNING_MESSAGE);
494 switch (confirmed) {
495 case JOptionPane.YES_OPTION:
496 return saveModel();
497 case JOptionPane.NO_OPTION:
498 return true;
499 case JOptionPane.CANCEL_OPTION:
500 default:
501 return false;
502 }
503 }
504
505 private File selectFileOpen() {
506 // create a file chooser
507 JFileChooser chooser = new JFileChooser(curDir);
508 // ask for a file to open
509 int option = chooser.showOpenDialog(this);
510 if (option == JFileChooser.APPROVE_OPTION) {
511 // remember selected directory
512 curDir = chooser.getCurrentDirectory();
513 // and return selected file to open
514 return chooser.getSelectedFile();
515 } else {
516 // no file selected, return null
517 return null;
518 }
519 }
520
521 private File selectFileSave() {
522 // create a file chooser
523 JFileChooser chooser = new JFileChooser(curDir);
524 if (curSaveFile != null) chooser.setSelectedFile(curSaveFile);
525 // overwrite check loop
526 while (true) {
527 int option = chooser.showSaveDialog(this);
528 if (option == JFileChooser.APPROVE_OPTION) {
529 // check for existing selected file
530 if (chooser.getSelectedFile().exists()) {
531 int confirm = JOptionPane.showConfirmDialog(this,
532 "Overwrite existing "+
533 chooser.getSelectedFile().getName()+"?",
534 "Confirm File Overwrite",
535 JOptionPane.YES_NO_OPTION,
536 JOptionPane.WARNING_MESSAGE);
537 if (confirm == JOptionPane.YES_OPTION) {
538 // confirmed overwrite, so no repeat
539 break;
540 }
541 } else {
542 // file doesn't exist, so don't repeat
543 break;
544 }
545 } else {
546 // not approved, so return null
547 return null;
548 }
549 }
550
551 // remember selected directory and savefile
552 curDir = chooser.getCurrentDirectory();
553 curSaveFile = chooser.getSelectedFile();
554 // return file
555 return curSaveFile;
556 }
557
558 public void setDefaultSaveFile(File file) {
559 this.curSaveFile = file;
560 }
561
562 public static class SplashScreen extends JWindow implements StatusView {
563 JTextArea statusArea;
564
565 public SplashScreen() {
566 super();
567
568 // get the splash graphic
569 JLabel splashLabel = new JLabel(IconService.getIcon("splash.gif"));
570
571 // create a status field
572 statusArea = new JTextArea();
573 statusArea.setFont(new Font("SansSerif",Font.PLAIN,10));
574 statusArea.setRows(6);
575 statusArea.setLineWrap(true);
576 statusArea.setWrapStyleWord(true);
577 statusArea.setEditable(false);
578
579 // add it to the frame
580 getContentPane().setLayout(new BorderLayout());
581 getContentPane().add(splashLabel,BorderLayout.CENTER);
582 getContentPane().add(new JScrollPane(statusArea),BorderLayout.SOUTH);
583
584 // display it
585 this.pack();
586 this.centerWindow();
587 this.show();
588 }
589
590 public void showStatus(final String status) {
591 SwingUtilities.invokeLater(new Runnable() {
592 public void run() {
593 statusArea.append(status+"\n");
594 statusArea.setCaretPosition(statusArea.getText().length()-1);
595 }
596 });
597 }
598
599 private void centerWindow() {
600 Rectangle virtualBounds = new Rectangle();
601 GraphicsEnvironment ge =
602 GraphicsEnvironment.getLocalGraphicsEnvironment();
603 GraphicsDevice[] gs = ge.getScreenDevices();
604 for (int j=0;j<gs.length;j++) {
605 GraphicsDevice gd = gs[j];
606 GraphicsConfiguration[] gc = gd.getConfigurations();
607 for (int i=0; i < gc.length; i++) {
608 virtualBounds = virtualBounds.union(gc[i].getBounds());
609 }
610 }
611
612 int cx = (int) virtualBounds.getWidth()/2;
613 int cy = (int) virtualBounds.getHeight()/2;
614 this.setLocation(cx - this.getWidth()/2,
615 cy - this.getHeight()/2);
616 }
617 }
618 }