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    }