001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.gui;
020:
021: import java.awt.BorderLayout;
022: import java.awt.Component;
023: import java.awt.Dimension;
024: import java.awt.Font;
025: import java.awt.Insets;
026: import java.awt.event.ActionEvent;
027: import java.awt.event.ActionListener;
028: import java.awt.event.MouseEvent;
029: import java.awt.event.WindowAdapter;
030: import java.awt.event.WindowEvent;
031: import java.util.HashSet;
032: import java.util.Set;
033:
034: import javax.swing.BorderFactory;
035: import javax.swing.Box;
036: import javax.swing.BoxLayout;
037: import javax.swing.ImageIcon;
038: import javax.swing.JButton;
039: import javax.swing.JComponent;
040: import javax.swing.JDialog;
041: import javax.swing.JFrame;
042: import javax.swing.JLabel;
043: import javax.swing.JMenu;
044: import javax.swing.JPanel;
045: import javax.swing.JPopupMenu;
046: import javax.swing.JScrollPane;
047: import javax.swing.JSplitPane;
048: import javax.swing.JTree;
049: import javax.swing.MenuElement;
050: import javax.swing.SwingUtilities;
051: import javax.swing.tree.DefaultMutableTreeNode;
052: import javax.swing.tree.DefaultTreeCellRenderer;
053: import javax.swing.tree.TreeCellRenderer;
054: import javax.swing.tree.TreeModel;
055: import javax.swing.tree.TreePath;
056:
057: import org.apache.jmeter.engine.event.LoopIterationEvent;
058: import org.apache.jmeter.gui.action.ActionNames;
059: import org.apache.jmeter.gui.action.ActionRouter;
060: import org.apache.jmeter.gui.tree.JMeterCellRenderer;
061: import org.apache.jmeter.gui.tree.JMeterTreeListener;
062: import org.apache.jmeter.gui.util.JMeterMenuBar;
063: import org.apache.jmeter.samplers.Remoteable;
064: import org.apache.jmeter.testelement.TestElement;
065: import org.apache.jmeter.testelement.TestListener;
066: import org.apache.jmeter.threads.JMeterContextService;
067: import org.apache.jmeter.util.JMeterUtils;
068: import org.apache.jorphan.gui.ComponentUtil;
069:
070: /**
071: * The main JMeter frame, containing the menu bar, test tree, and an area for
072: * JMeter component GUIs.
073: *
074: */
075: public class MainFrame extends JFrame implements TestListener,
076: Remoteable {
077:
078: // This is used to keep track of local (non-remote) tests
079: // The name is chosen to be an unlikely host-name
080: private static final String LOCAL = "*local*"; // $NON-NLS-1$
081:
082: // The default title for the Menu bar
083: private static final String DEFAULT_TITLE = "Apache JMeter ("
084: + JMeterUtils.getJMeterVersion() + ")"; // $NON-NLS-1$ $NON-NLS-2$
085:
086: /** The menu bar. */
087: private JMeterMenuBar menuBar;
088:
089: /** The main panel where components display their GUIs. */
090: private JScrollPane mainPanel;
091:
092: /** The panel where the test tree is shown. */
093: private JScrollPane treePanel;
094:
095: /** The test tree. */
096: private JTree tree;
097:
098: /** An image which is displayed when a test is running. */
099: private ImageIcon runningIcon = JMeterUtils
100: .getImage("thread.enabled.gif");// $NON-NLS-1$
101:
102: /** An image which is displayed when a test is not currently running. */
103: private ImageIcon stoppedIcon = JMeterUtils
104: .getImage("thread.disabled.gif");// $NON-NLS-1$
105:
106: /** The button used to display the running/stopped image. */
107: private JButton runningIndicator;
108:
109: /** The x coordinate of the last location where a component was dragged. */
110: private int previousDragXLocation = 0;
111:
112: /** The y coordinate of the last location where a component was dragged. */
113: private int previousDragYLocation = 0;
114:
115: /** The set of currently running hosts. */
116: private Set hosts = new HashSet();
117:
118: /** A message dialog shown while JMeter threads are stopping. */
119: private JDialog stoppingMessage;
120:
121: private JLabel totalThreads;
122: private JLabel activeThreads;
123:
124: /**
125: * Create a new JMeter frame.
126: *
127: * @param actionHandler
128: * this parameter is not used
129: * @param treeModel
130: * the model for the test tree
131: * @param treeListener
132: * the listener for the test tree
133: */
134: public MainFrame(ActionListener actionHandler, TreeModel treeModel,
135: JMeterTreeListener treeListener) {
136: // TODO: actionHandler isn't used -- remove it from the parameter list
137: // this.actionHandler = actionHandler;
138:
139: // TODO: Make the running indicator its own class instead of a JButton
140: runningIndicator = new JButton(stoppedIcon);
141: runningIndicator.setMargin(new Insets(0, 0, 0, 0));
142: runningIndicator.setBorder(BorderFactory.createEmptyBorder());
143:
144: totalThreads = new JLabel("0"); // $NON-NLS-1$
145: activeThreads = new JLabel("0"); // $NON-NLS-1$
146:
147: tree = makeTree(treeModel, treeListener);
148:
149: GuiPackage.getInstance().setMainFrame(this );
150: init();
151:
152: setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
153: }
154:
155: /**
156: * Default constructor for the JMeter frame. This constructor will not
157: * properly initialize the tree, so don't use it.
158: *
159: * @deprecated Do not use - only needed for JUnit tests
160: */
161: public MainFrame() {
162: }
163:
164: // MenuBar related methods
165: // TODO: Do we really need to have all these menubar methods duplicated
166: // here? Perhaps we can make the menu bar accessible through GuiPackage?
167:
168: /**
169: * Specify whether or not the File|Load menu item should be enabled.
170: *
171: * @param enabled
172: * true if the menu item should be enabled, false otherwise
173: */
174: public void setFileLoadEnabled(boolean enabled) {
175: menuBar.setFileLoadEnabled(enabled);
176: }
177:
178: /**
179: * Specify whether or not the File|Save menu item should be enabled.
180: *
181: * @param enabled
182: * true if the menu item should be enabled, false otherwise
183: */
184: public void setFileSaveEnabled(boolean enabled) {
185: menuBar.setFileSaveEnabled(enabled);
186: }
187:
188: /**
189: * Specify whether or not the File|Revert item should be enabled.
190: *
191: * @param enabled
192: * true if the menu item should be enabled, false otherwise
193: */
194: public void setFileRevertEnabled(boolean enabled) {
195: menuBar.setFileRevertEnabled(enabled);
196: }
197:
198: /**
199: * Specify the project file that was just loaded
200: *
201: * @param file - the full path to the file that was loaded
202: */
203: public void setProjectFileLoaded(String file) {
204: menuBar.setProjectFileLoaded(file);
205: }
206:
207: /**
208: * Set the menu that should be used for the Edit menu.
209: *
210: * @param menu
211: * the new Edit menu
212: */
213: public void setEditMenu(JPopupMenu menu) {
214: menuBar.setEditMenu(menu);
215: }
216:
217: /**
218: * Specify whether or not the Edit menu item should be enabled.
219: *
220: * @param enabled
221: * true if the menu item should be enabled, false otherwise
222: */
223: public void setEditEnabled(boolean enabled) {
224: menuBar.setEditEnabled(enabled);
225: }
226:
227: /**
228: * Set the menu that should be used for the Edit|Add menu.
229: *
230: * @param menu
231: * the new Edit|Add menu
232: */
233: public void setEditAddMenu(JMenu menu) {
234: menuBar.setEditAddMenu(menu);
235: }
236:
237: /**
238: * Specify whether or not the Edit|Add menu item should be enabled.
239: *
240: * @param enabled
241: * true if the menu item should be enabled, false otherwise
242: */
243: public void setEditAddEnabled(boolean enabled) {
244: menuBar.setEditAddEnabled(enabled);
245: }
246:
247: /**
248: * Specify whether or not the Edit|Remove menu item should be enabled.
249: *
250: * @param enabled
251: * true if the menu item should be enabled, false otherwise
252: */
253: public void setEditRemoveEnabled(boolean enabled) {
254: menuBar.setEditRemoveEnabled(enabled);
255: }
256:
257: /**
258: * Close the currently selected menu.
259: */
260: public void closeMenu() {
261: if (menuBar.isSelected()) {
262: MenuElement[] menuElement = menuBar.getSubElements();
263: if (menuElement != null) {
264: for (int i = 0; i < menuElement.length; i++) {
265: JMenu menu = (JMenu) menuElement[i];
266: if (menu.isSelected()) {
267: menu.setPopupMenuVisible(false);
268: menu.setSelected(false);
269: break;
270: }
271: }
272: }
273: }
274: }
275:
276: /**
277: * Show a dialog indicating that JMeter threads are stopping on a particular
278: * host.
279: *
280: * @param host
281: * the host where JMeter threads are stopping
282: */
283: public void showStoppingMessage(String host) {
284: stoppingMessage = new JDialog(this , JMeterUtils
285: .getResString("stopping_test_title"), true); //$NON-NLS-1$
286: JLabel stopLabel = new JLabel(JMeterUtils
287: .getResString("stopping_test") + ": " + host); //$NON-NLS-1$$NON-NLS-2$
288: stopLabel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20,
289: 20));
290: stoppingMessage.getContentPane().add(stopLabel);
291: stoppingMessage.pack();
292: ComponentUtil.centerComponentInComponent(this , stoppingMessage);
293: SwingUtilities.invokeLater(new Runnable() {
294: public void run() {
295: if (stoppingMessage != null) {// TODO - how can this be null?
296: stoppingMessage.show();
297: }
298: }
299: });
300: }
301:
302: public void updateCounts() {
303: SwingUtilities.invokeLater(new Runnable() {
304: public void run() {
305: activeThreads.setText(Integer
306: .toString(JMeterContextService
307: .getNumberOfThreads()));
308: totalThreads.setText(Integer
309: .toString(JMeterContextService
310: .getTotalThreads()));
311: }
312: });
313: }
314:
315: public void setMainPanel(JComponent comp) {
316: mainPanel.setViewportView(comp);
317: }
318:
319: public JTree getTree() {
320: return tree;
321: }
322:
323: // TestListener implementation
324:
325: /**
326: * Called when a test is started on the local system. This implementation
327: * sets the running indicator and ensures that the menubar is enabled and in
328: * the running state.
329: */
330: public void testStarted() {
331: testStarted(LOCAL);
332: menuBar.setEnabled(true);
333: }
334:
335: /**
336: * Called when a test is started on a specific host. This implementation
337: * sets the running indicator and ensures that the menubar is in the running
338: * state.
339: *
340: * @param host
341: * the host where the test is starting
342: */
343: public void testStarted(String host) {
344: hosts.add(host);
345: runningIndicator.setIcon(runningIcon);
346: activeThreads.setText("0"); // $NON-NLS-1$
347: totalThreads.setText("0"); // $NON-NLS-1$
348: menuBar.setRunning(true, host);
349: }
350:
351: /**
352: * Called when a test is ended on the local system. This implementation
353: * disables the menubar, stops the running indicator, and closes the
354: * stopping message dialog.
355: */
356: public void testEnded() {
357: testEnded(LOCAL);
358: menuBar.setEnabled(false);
359: }
360:
361: /**
362: * Called when a test is ended on the remote system. This implementation
363: * stops the running indicator and closes the stopping message dialog.
364: *
365: * @param host
366: * the host where the test is ending
367: */
368: public void testEnded(String host) {
369: hosts.remove(host);
370: if (hosts.size() == 0) {
371: runningIndicator.setIcon(stoppedIcon);
372: JMeterContextService.endTest();
373: }
374: menuBar.setRunning(false, host);
375: if (stoppingMessage != null) {
376: stoppingMessage.dispose();
377: stoppingMessage = null;
378: }
379: activeThreads.setText("0");
380: totalThreads.setText("0");
381: }
382:
383: /* Implements TestListener#testIterationStart(LoopIterationEvent) */
384: public void testIterationStart(LoopIterationEvent event) {
385: }
386:
387: /**
388: * Create the GUI components and layout.
389: */
390: private void init() {
391: menuBar = new JMeterMenuBar();
392: setJMenuBar(menuBar);
393:
394: JPanel all = new JPanel(new BorderLayout());
395: all.add(createToolBar(), BorderLayout.NORTH);
396:
397: JSplitPane treeAndMain = new JSplitPane(
398: JSplitPane.HORIZONTAL_SPLIT);
399:
400: treePanel = createTreePanel();
401: treeAndMain.setLeftComponent(treePanel);
402:
403: mainPanel = createMainPanel();
404: treeAndMain.setRightComponent(mainPanel);
405:
406: treeAndMain.setResizeWeight(.2);
407: treeAndMain.setContinuousLayout(true);
408: all.add(treeAndMain, BorderLayout.CENTER);
409:
410: getContentPane().add(all);
411:
412: tree.setSelectionRow(1);
413: addWindowListener(new WindowHappenings());
414:
415: setTitle(DEFAULT_TITLE);
416: setIconImage(JMeterUtils.getImage("jmeter.jpg").getImage());// $NON-NLS-1$
417: }
418:
419: public void setExtendedFrameTitle(String fname) {
420: // file New operation may set to null, so just return app name
421: if (fname == null) {
422: setTitle(DEFAULT_TITLE);
423: return;
424: }
425:
426: // allow for windows / chars in filename
427: String temp = fname.replace('\\', '/'); // $NON-NLS-1$ // $NON-NLS-2$
428: String simpleName = temp.substring(temp.lastIndexOf("/") + 1);// $NON-NLS-1$
429: setTitle(simpleName + " (" + fname + ") - " + DEFAULT_TITLE); // $NON-NLS-1$ // $NON-NLS-2$
430: }
431:
432: /**
433: * Create the JMeter tool bar pane containing the running indicator.
434: *
435: * @return a panel containing the running indicator
436: */
437: private Component createToolBar() {
438: Box toolPanel = new Box(BoxLayout.X_AXIS);
439: toolPanel.add(Box.createRigidArea(new Dimension(10, 15)));
440: toolPanel.add(Box.createGlue());
441: toolPanel.add(activeThreads);
442: toolPanel.add(new JLabel(" / "));
443: toolPanel.add(totalThreads);
444: toolPanel.add(Box.createRigidArea(new Dimension(10, 15)));
445: toolPanel.add(runningIndicator);
446: return toolPanel;
447: }
448:
449: /**
450: * Create the panel where the GUI representation of the test tree is
451: * displayed. The tree should already be created before calling this method.
452: *
453: * @return a scroll pane containing the test tree GUI
454: */
455: private JScrollPane createTreePanel() {
456: JScrollPane treeP = new JScrollPane(tree);
457: treeP.setMinimumSize(new Dimension(100, 0));
458: return treeP;
459: }
460:
461: /**
462: * Create the main panel where components can display their GUIs.
463: *
464: * @return the main scroll pane
465: */
466: private JScrollPane createMainPanel() {
467: return new JScrollPane();
468: }
469:
470: /**
471: * Create and initialize the GUI representation of the test tree.
472: *
473: * @param treeModel
474: * the test tree model
475: * @param treeListener
476: * the test tree listener
477: *
478: * @return the initialized test tree GUI
479: */
480: private JTree makeTree(TreeModel treeModel,
481: JMeterTreeListener treeListener) {
482: JTree treevar = new JTree(treeModel) {
483: public String getToolTipText(MouseEvent event) {
484: TreePath path = this .getPathForLocation(event.getX(),
485: event.getY());
486: if (path != null) {
487: Object treeNode = path.getLastPathComponent();
488: if (treeNode instanceof DefaultMutableTreeNode) {
489: Object testElement = ((DefaultMutableTreeNode) treeNode)
490: .getUserObject();
491: if (testElement instanceof TestElement) {
492: String comment = ((TestElement) testElement)
493: .getComment();
494: if (comment != null && comment.length() > 0) {
495: return comment;
496: }
497: }
498: }
499: }
500: return null;
501: }
502: };
503: treevar.setToolTipText("");
504: treevar.setCellRenderer(getCellRenderer());
505: treevar.setRootVisible(false);
506: treevar.setShowsRootHandles(true);
507:
508: treeListener.setJTree(treevar);
509: treevar.addTreeSelectionListener(treeListener);
510: treevar.addMouseListener(treeListener);
511: treevar.addMouseMotionListener(treeListener);
512: treevar.addKeyListener(treeListener);
513:
514: return treevar;
515: }
516:
517: /**
518: * Create the tree cell renderer used to draw the nodes in the test tree.
519: *
520: * @return a renderer to draw the test tree nodes
521: */
522: private TreeCellRenderer getCellRenderer() {
523: DefaultTreeCellRenderer rend = new JMeterCellRenderer();
524: rend.setFont(new Font("Dialog", Font.PLAIN, 11));
525: return rend;
526: }
527:
528: /**
529: * Repaint pieces of the GUI as needed while dragging. This method should
530: * only be called from the Swing event thread.
531: *
532: * @param dragIcon
533: * the component being dragged
534: * @param x
535: * the current mouse x coordinate
536: * @param y
537: * the current mouse y coordinate
538: */
539: public void drawDraggedComponent(Component dragIcon, int x, int y) {
540: Dimension size = dragIcon.getPreferredSize();
541: treePanel.paintImmediately(previousDragXLocation,
542: previousDragYLocation, size.width, size.height);
543: this .getLayeredPane().setLayer(dragIcon, 400);
544: SwingUtilities.paintComponent(treePanel.getGraphics(),
545: dragIcon, treePanel, x, y, size.width, size.height);
546: previousDragXLocation = x;
547: previousDragYLocation = y;
548: }
549:
550: /**
551: * A window adapter used to detect when the main JMeter frame is being
552: * closed.
553: */
554: private static class WindowHappenings extends WindowAdapter {
555: /**
556: * Called when the main JMeter frame is being closed. Sends a
557: * notification so that JMeter can react appropriately.
558: *
559: * @param event
560: * the WindowEvent to handle
561: */
562: public void windowClosing(WindowEvent event) {
563: ActionRouter.getInstance().actionPerformed(
564: new ActionEvent(this, event.getID(),
565: ActionNames.EXIT));
566: }
567: }
568: }
|