001 package aima.gui.framework; 002 003 import java.awt.BorderLayout; 004 import java.awt.event.ActionEvent; 005 import java.awt.event.ActionListener; 006 import java.io.PrintStream; 007 import java.util.ArrayList; 008 import java.util.List; 009 010 import javax.swing.Box; 011 import javax.swing.JButton; 012 import javax.swing.JComboBox; 013 import javax.swing.JLabel; 014 import javax.swing.JPanel; 015 import javax.swing.JScrollPane; 016 import javax.swing.JSplitPane; 017 import javax.swing.JTextArea; 018 import javax.swing.JToolBar; 019 import javax.swing.SwingUtilities; 020 021 /** 022 * <p> 023 * Universal frame for building graphical agent applications. It provides 024 * buttons for controlling the application and two panels and a status bar to 025 * visualize agent, environment, and general simulation state. 026 * </p> 027 * <p> 028 * To make the frame fit to your needs, you will at least have to add some 029 * selectors and replace the dummy agent view pane by a view which is capable to 030 * visualize your agent. The frame is configurable at run-time, so subclassing 031 * will not always be necessary. 032 * </p> 033 * 034 * @author R. Lunde 035 */ 036 public class AgentAppFrame extends javax.swing.JFrame implements 037 AgentAppModel.ModelChangedListener { 038 039 /** The controller, which executes the domain-level commands. */ 040 protected Controller controller; 041 /** The model, which provides data about agent and environment state. */ 042 private AgentAppModel model; 043 /** Extra waiting time after each model change, used for animation. */ 044 private int updateDelay; 045 /** Thread running the agent. */ 046 protected AgentThread agentThread; 047 /** Flag, indicating whether the agent is ready for running. */ 048 protected boolean isPrepared; 049 /** Contains selector specification and resulting comboboxes. */ 050 private SelectorContainer selectors; 051 052 private JToolBar toolbar; 053 private JButton clearButton; 054 private JButton prepareButton; 055 private JButton runButton; 056 private JButton cancelButton; 057 058 JSplitPane centerPane; 059 protected JTextArea textArea; 060 protected AbstractAgentView agentView; 061 062 private JLabel statusLabel; 063 064 /** Standard constructor. */ 065 public AgentAppFrame() { 066 initComponents(); 067 pack(); 068 // redirect the standard output into the text area 069 System.setOut(new PrintStream(new TextAreaOutputStream())); 070 // System.setErr(new PrintStream(new TextAreaOutputStream())); 071 updateDelay = 0; 072 setButtonsEnabled(true); 073 } 074 075 /** 076 * Specifies a set of combo boxes to be added to the toolbar. Each combobox 077 * has a name, which is used to access its selection state on software level 078 * and optionally a tool tip, which is shown to the user. 079 * 080 * @param tooltips 081 * Array of strings or null. 082 * 083 */ 084 public void setSelectors(String[] selectorNames, String[] tooltips) { 085 Controller cont = controller; 086 controller = null; // suppress reactions on parameter changes. 087 selectors.setSelectors(selectorNames, tooltips); 088 controller = cont; 089 } 090 091 /** 092 * Sets the choice items and the default value of a specified selector. The 093 * first item has index 0. 094 */ 095 public void setSelectorItems(String selectorName, String[] items, 096 int defaultIdx) { 097 Controller cont = controller; 098 controller = null; // suppress reactions on parameter changes. 099 selectors.setSelectorItems(selectorName, items, defaultIdx); 100 controller = cont; 101 } 102 103 /** Adjusts selection state of all selectors. */ 104 public void setDefaultSelection() { 105 Controller cont = controller; 106 controller = null; // suppress reactions on parameter changes. 107 selectors.setDefaults(); 108 if (cont != null) { 109 controller = cont; 110 selectionChanged(); 111 } 112 } 113 114 /** 115 * Returns an object which represents the current selection state of all 116 * selectors. 117 */ 118 public SelectionState getSelection() { 119 return selectors.getSelection(); 120 } 121 122 /** 123 * Template method, replacing the agent view. The agent view is the panel to 124 * the left of the splitbar. It typically implements a 2D-visualization of 125 * the agent in its environment. 126 */ 127 public void setAgentView(AbstractAgentView view) { 128 agentView = view; 129 centerPane.add(centerPane.LEFT, agentView); 130 } 131 132 /** Specifies how to distribute extra space when resizing the split pane. */ 133 public void setSplitPaneResizeWeight(double value) { 134 centerPane.setResizeWeight(value); 135 } 136 137 /** Defines, who should react on button and selection events. */ 138 public void setController(Controller controller) { 139 this.controller = controller; 140 } 141 142 /** Gives the frame access to agent and environment data. */ 143 public void setModel(AgentAppModel model) { 144 this.model = model; 145 agentView.updateView(model); 146 } 147 148 /** Sets a delay for displaying model changes. Used for animation. */ 149 public void setUpdateDelay(int msec) { 150 updateDelay = msec; 151 } 152 153 /** Displays a text in the status bar. */ 154 public void setStatus(String status) { 155 statusLabel.setText(status); 156 } 157 158 /** Updates the agent view after the state of the model has changed. */ 159 public void modelChanged() { 160 agentView.updateView(model); 161 try { 162 Thread.sleep(updateDelay); 163 } catch (Exception e) { 164 logMessage("Error: Something went wrong when updating " 165 + "the view after a model change (" + e + ")."); 166 e.printStackTrace(); 167 } 168 } 169 170 /** Prints a log message on the text area. */ 171 public void logMessage(String message) { 172 MessageLogger ml = new MessageLogger(); 173 ml.message = message; 174 if (SwingUtilities.isEventDispatchThread()) { 175 ml.run(); 176 } else { 177 try { 178 SwingUtilities.invokeAndWait(ml); 179 } catch (Exception e) { 180 e.printStackTrace(); 181 } 182 } 183 } 184 185 /** Helper class which makes logging thread save. */ 186 private class MessageLogger implements Runnable { 187 String message; 188 public void run() { 189 int start = textArea.getDocument().getLength(); 190 textArea.append(message + "\n"); 191 int end = textArea.getDocument().getLength(); 192 textArea.setSelectionStart(start); 193 textArea.setSelectionEnd(end); 194 } 195 } 196 197 /** Assembles the inner structure of the frame. */ 198 private void initComponents() { 199 addWindowListener(new java.awt.event.WindowAdapter() { 200 @Override 201 public void windowClosing(java.awt.event.WindowEvent evt) { 202 System.exit(0); 203 } 204 }); 205 toolbar = new JToolBar(); 206 // toolbar.setFloatable(false); 207 selectors = new SelectorContainer(); 208 // toolbar.add(selectors.selectorPanel); 209 toolbar.add(Box.createHorizontalGlue()); 210 211 clearButton = new JButton("Clear"); 212 clearButton.setToolTipText("Clear Views"); 213 clearButton.addActionListener(new FrameActionListener()); 214 toolbar.add(clearButton); 215 prepareButton = new JButton("Prepare"); 216 prepareButton.setToolTipText("Prepare Agent"); 217 prepareButton.addActionListener(new FrameActionListener()); 218 toolbar.add(prepareButton); 219 runButton = new JButton("Run"); 220 runButton.setToolTipText("Run Agent"); 221 runButton.addActionListener(new FrameActionListener()); 222 toolbar.add(runButton); 223 getContentPane().add(toolbar, java.awt.BorderLayout.NORTH); 224 225 textArea = new JTextArea(); 226 textArea.setEditable(false); 227 JScrollPane scrollTPane = new JScrollPane(textArea); 228 // scrollTPane.setPreferredSize(new java.awt.Dimension(900, 600)); 229 agentView = new AbstractAgentView() { 230 @Override 231 public void updateView(AgentAppModel model) { 232 } // dummy 233 // implementation 234 }; 235 JScrollPane scrollGPane = new JScrollPane(agentView); 236 centerPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); 237 // centerPane.setDividerLocation(0.7); 238 centerPane.add(JSplitPane.LEFT, scrollGPane); 239 centerPane.add(JSplitPane.RIGHT, scrollTPane); 240 centerPane.setDividerSize(5); 241 centerPane.setResizeWeight(0.8); 242 getContentPane().add(centerPane, BorderLayout.CENTER); 243 244 JPanel statusPanel = new JPanel(new BorderLayout()); 245 statusLabel = new JLabel(""); 246 statusLabel.setBorder(new javax.swing.border.EtchedBorder()); // BevelBorder(javax.swing.border.BevelBorder.RAISED)); 247 statusPanel.add(statusLabel, BorderLayout.CENTER); 248 cancelButton = new JButton("Cancel"); 249 cancelButton.setToolTipText("Cancel Agent"); 250 cancelButton.addActionListener(new FrameActionListener()); 251 cancelButton.setPreferredSize(new java.awt.Dimension(80, 20)); 252 cancelButton.setBorder(new javax.swing.border.EtchedBorder()); 253 statusPanel.add(cancelButton, BorderLayout.EAST); 254 getContentPane().add(statusPanel, BorderLayout.SOUTH); 255 } 256 257 /** Enables/disables all combos and buttons. */ 258 private void setButtonsEnabled(boolean b) { 259 clearButton.setEnabled(b); 260 prepareButton.setEnabled(b); 261 runButton.setEnabled(b); 262 cancelButton.setEnabled(!b); 263 for (JComboBox combo : selectors.combos) 264 combo.setEnabled(b); 265 } 266 267 /** Tells the controller to prepare the agent. */ 268 protected void selectionChanged() { 269 if (controller != null) { 270 controller.prepareAgent(); 271 isPrepared = true; 272 } 273 } 274 275 // //////////////////////////////////////////////////////// 276 // inner classes 277 278 /** Sends commands to the controller. */ 279 private class FrameActionListener implements ActionListener { 280 public void actionPerformed(ActionEvent evt) { 281 String text = ""; 282 try { 283 if (controller != null) { 284 setStatus(""); 285 Object source = evt.getSource(); 286 if (source == clearButton /* || source == clearMenuItem */) { 287 // additionally clear the text area 288 text = "when clearing the views "; 289 javax.swing.text.Document doc = textArea.getDocument(); 290 doc.remove(0, doc.getLength()); 291 statusLabel.setText(""); 292 controller.clearAgent(); 293 // isPrepared = false; 294 } else if (source == prepareButton) { 295 text = "when preparing the agent "; 296 controller.prepareAgent(); 297 isPrepared = true; 298 } else if (source == runButton) { 299 text = "when preparing the agent "; 300 if (isPrepared == false) 301 controller.prepareAgent(); 302 text = "when running the agent "; 303 setButtonsEnabled(false); 304 agentThread = new AgentThread(); 305 agentThread.start(); 306 isPrepared = false; 307 } else if (source == cancelButton) { 308 text = "when cancelling the agent "; 309 if (agentThread != null) { 310 agentThread.stop(); // quick and dirty! 311 agentThread = null; 312 setStatus("Task cancelled."); 313 setButtonsEnabled(true); 314 } 315 isPrepared = false; 316 } else if (selectors.combos.contains(source)) { 317 text = "when preparing the agent "; 318 selectionChanged(); 319 } 320 } 321 } catch (Exception e) { 322 logMessage("Error: Something went wrong " + text + "(" + e 323 + ")."); 324 e.printStackTrace(); 325 } 326 } 327 } 328 329 /** Thread, which helps to perform GUI updates during simulation. */ 330 private class AgentThread extends Thread { 331 @Override 332 public void run() { 333 agentThread = this; 334 try { 335 controller.runAgent(); 336 } catch (Exception e) { 337 logMessage("Error: Somthing went wrong running the agent (" + e 338 + ")."); 339 e.printStackTrace(); // for debugging 340 } 341 setButtonsEnabled(true); 342 agentThread = null; 343 } 344 } 345 346 /** Writes everything into the text area. */ 347 private class TextAreaOutputStream extends java.io.OutputStream { 348 @Override 349 public void write(int b) throws java.io.IOException { 350 String s = new String(new char[] { (char) b }); 351 textArea.append(s); 352 } 353 } 354 355 /** Maintains all selector comboboxes. */ 356 private class SelectorContainer { 357 String[] selectorNames = new String[] {}; 358 int[] selectorDefaults = new int[] {}; 359 // JPanel selectorPanel = new JPanel(); 360 List<JComboBox> combos = new ArrayList<JComboBox>(); 361 362 public void setSelectors(String[] selectorNames, String[] tooltips) { 363 this.selectorNames = selectorNames; 364 this.selectorDefaults = new int[selectorNames.length]; 365 for (JComboBox combo : combos) 366 toolbar.remove(combo); 367 combos.clear(); 368 for (int i = 0; i < selectorNames.length; i++) { 369 JComboBox combo = new JComboBox(); 370 combo.addActionListener(new FrameActionListener()); 371 combos.add(combo); 372 toolbar.add(combo, i); 373 if (tooltips != null) 374 combo.setToolTipText(tooltips[i]); 375 } 376 } 377 378 public void setSelectorItems(String selectorName, String[] items, 379 int defaultIdx) { 380 JComboBox combo = getCombo(selectorName); 381 combo.removeAllItems(); 382 for (String item : items) 383 combo.addItem(item); 384 selectorDefaults[combos.indexOf(combo)] = defaultIdx; 385 } 386 387 public void setDefaults() { 388 for (int i = 0; i < selectorDefaults.length; i++) { 389 if (combos.get(i).getItemCount() > 0) 390 combos.get(i).setSelectedIndex(selectorDefaults[i]); 391 } 392 } 393 394 public SelectionState getSelection() { 395 SelectionState result = new SelectionState(selectorNames); 396 for (int i = 0; i < result.size(); i++) { 397 result.setValue(i, combos.get(i).getSelectedIndex()); 398 } 399 return result; 400 } 401 402 JComboBox getCombo(String selectorName) { 403 for (int i = 0; i < selectorNames.length; i++) 404 if (selectorNames[i].equals(selectorName)) 405 return combos.get(i); 406 return null; 407 } 408 } 409 410 // //////////////////////////////////////////////////////// 411 // static inner classes 412 413 /** 414 * Contains the names of all selectors and the indices of their selected 415 * items. Instances are used to communicate the selection state between the 416 * frame and the controller. 417 */ 418 public static class SelectionState { 419 private final List<String> selectors = new ArrayList<String>(); 420 private final List<Integer> selIndices = new ArrayList<Integer>(); 421 422 protected SelectionState(String[] selectors) { 423 for (String sel : selectors) { 424 this.selectors.add(sel); 425 this.selIndices.add(null); 426 } 427 } 428 429 /** Returns the number of selectors currently available. */ 430 public int size() { 431 return selectors.size(); 432 } 433 434 /** Sets the selection state of a specified selector to a specified item. */ 435 void setValue(int selectorIdx, int valIdx) { 436 selIndices.set(selectorIdx, valIdx); 437 } 438 439 /** Sets the selection state of a specified selector to a specified item. */ 440 void setValue(String selector, int valIdx) { 441 selIndices.set(selectors.indexOf(selector), valIdx); 442 } 443 444 /** Returns the index of the selected item of a specified selector. */ 445 public int getValue(int selectorIdx) { 446 return selIndices.get(selectorIdx); 447 } 448 449 /** Returns the index of the selected item of a specified selector. */ 450 public int getValue(String selector) { 451 return selIndices.get(selectors.indexOf(selector)); 452 } 453 454 /** Returns a readable representation of the selection state. */ 455 @Override 456 public String toString() { 457 StringBuffer result = new StringBuffer("State[ "); 458 for (int i = 0; i < size(); i++) 459 result.append(selectors.get(i) + "=" + selIndices.get(i) + " "); 460 result.append("]"); 461 return result.toString(); 462 } 463 } 464 465 /** Base class for all agent views. */ 466 public static abstract class AbstractAgentView extends JPanel { 467 /** Called by the agent application frame after model changes. */ 468 public abstract void updateView(AgentAppModel model); 469 } 470 471 /** 472 * The agent application frame delegates the execution of all domain-level 473 * commands to a controller. Any class implementing this interface is in 474 * principle suitable. 475 */ 476 public static interface Controller { 477 public abstract void clearAgent(); 478 479 public abstract void prepareAgent(); 480 481 public abstract void runAgent(); 482 } 483 484 // /////////////////////////////////////////////////////////////// 485 // for testing... 486 487 public static void main(String[] args) { 488 AgentAppFrame frame = new AgentAppFrame(); 489 frame.setVisible(true); 490 } 491 }