/* Author: Jeff Dalton * Updated: Wed Dec 5 00:47:19 2001 by Jeff Dalton * Copyright: (c) 2001, AIAI, University of Edinburgh */ package ix.ideel; import javax.swing.*; import java.awt.Component; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.GridLayout; import java.awt.event.*; import java.util.*; import ix.ideel.event.*; import ix.ip2.Ip2Frame; import ix.icore.*; import ix.icore.process.StatusValues; import ix.iface.util.GridColumn; import ix.iface.util.PriorityComboBox; import ix.util.*; import ix.util.lisp.*; /** * A viewer for a set of IdeelIssues. The issues are managed by * an IssueManager (usually a Controller). */ public class IssueViewingTable extends JPanel implements IssueViewer, ControllerListener, StatusValues { protected IXAgent agent; IssueManager issueManager; IssueEditor issueEditor; GridColumn descriptionCol = new GridColumn("Description"); GridColumn commentsCol = new GridColumn("Annotations"); GridColumn priorityCol = new GridColumn("Priority"); GridColumn optionCol = new GridColumn("Action "); final int descriptionWidth = Parameters.getInt("description-column-width", 30); final int commentsWidth = Parameters.getInt("annotations-column-width", 20); // Things are a bit awkward because, now that the table is organized // by column, we no longer have row objects. The following tables // nonetheless let us determine where to insert the column entries // for an issue that has a parent. HashMap issueToRowTable = new HashMap(); HashMap rowToIssueTable = new HashMap(); /** * Says which option to use when more than one have the same * description. */ TwoKeyHashMap optionShadowingTable = new TwoKeyHashMap(); public IssueViewingTable(IXAgent agent) { super(); this.agent = agent; setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); descriptionCol.setPreferredSize (new JTextField("", descriptionWidth).getPreferredSize()); commentsCol.setPreferredSize (new JTextField("", commentsWidth).getPreferredSize()); priorityCol.setPreferredSize (new PriorityComboBox().getPreferredSize()); // { // JComboBox tmpBox = new JComboBox(); // tmpBox.addItem("Expand using test"); // tmpBox.setSelectedItem("Expand using test"); // optionCol.setPreferredSize(tmpBox.getPreferredSize()); // } add(descriptionCol); add(commentsCol); add(priorityCol); add(optionCol); } public synchronized void reset() { optionShadowingTable.clear(); issueToRowTable.clear(); rowToIssueTable.clear(); descriptionCol.reset(); commentsCol.reset(); priorityCol.reset(); optionCol.reset(); invalidate(); } public void setIssueManager(IssueManager issueManager) { this.issueManager = issueManager; } public void ensureIssueEditor() { if (issueEditor == null) { issueEditor = new IssueEditor (this, agent.getAgentSymbolName() + " Issue Editor"); } issueEditor.setVisible(true); } protected IssueOption findIssueOption(IdeelIssue issue, String shortDescription) { IssueOption shadowingOption = (IssueOption)optionShadowingTable.get(issue, shortDescription); return (shadowingOption != null) ? shadowingOption : issue.findOption(shortDescription); } protected void setShadowingOption(IdeelIssue issue, String shortDescription, IssueOption option) { optionShadowingTable.put(issue, shortDescription, option); } /* * Instructions from the issue editor */ public void saveIssueExpansion(Object data) { Ip2Frame root = (Ip2Frame)SwingUtilities.getRoot(this); root.getDomainEditor().saveExpansion(data); } public void expandIssue(IdeelIssue i, Object instructions) { issueManager.expandIssue(i, instructions); } public void newIssue() { ensureIssueEditor(); issueEditor.showNewIssue(); } public void addIssue(IdeelIssue i) { issueManager.addIssue(i); } public void addIssue(String text) { // Assumes that the text can be parsed w/o error. // Note that the IssueEditor does not call this method -- // it's called only "internally", for "test" issues. addIssue(new IdeelIssue(text)); } public void addIssue(int priority, String text) { // Assumes that the text can be parsed w/o error. // Note that the IssueEditor does not call this method -- // it's called only "internally", for "test" issues. IdeelIssue i = new IdeelIssue(text); i.setPriority(priority); addIssue(i); } /* * Instructions from the controller (aka issue manager) */ /* * Issue added */ // Method for ControllerListener interface public void issueAdded(ControllerEvent event, IdeelIssue i) { issueAdded(i); } public synchronized void issueAdded(final IdeelIssue i) { // if (!SwingUtilities.isEventDispatchThread()) { // // Happens when we get an issue from another agent // Debug.noteln("adding issue outside event dispatch thread"); // } // SwingUtilities.invokeLater(new Runnable() { // public void run() { // do_issueAdded(i); // } // }); // /\/: We now move to event dispatching thread in thw // IXAgent pre_handleInput method. So the order of events // will be less confusing if issue-adding doesn't get // moved here to the end of the queue. do_issueAdded(i); } protected void do_issueAdded(IdeelIssue i) { Debug.noteln("Viewer adding issue", i.shortDescription); // A JTextField that describes the issue. JTextField descriptionText = new JTextField(Util.repeatString(" ", i.level) + i.shortDescription, descriptionWidth); descriptionText.setCaretPosition(0); // show left edge // Probably should so something like this: // descriptionText.setEditable(false); // descriptionText.setBackground(Color.white); // but it doesn't look as nice. Adding a border seems to help, // but the code below doesn't produce an exact equivalent. descriptionText.setEditable(false); descriptionText.setBackground(Color.white); descriptionText.setBorder(BorderFactory.createEtchedBorder()); descriptionText.addMouseListener(makeMouseListener(i)); // A JTextField that shows the first line of comments. JTextField commentsText = new JTextField("", commentsWidth); if (!i.comments.equals("")) commentsText.setText(Util.firstLine(i.comments).trim()); commentsText.setEditable(false); commentsText.setBackground(Color.white); commentsText.setBorder(BorderFactory.createEtchedBorder()); commentsText.addMouseListener(makeMouseListener(i)); // Menu of Priorities PriorityComboBox priorityChoice = new PriorityComboBox(); priorityChoice.addActionListener(makePriorityChoiceListener(i)); priorityChoice.setPriority(i.getPriority()); // Menu of options for handling the issue. JComboBox optionChoice = new JComboBox(); for (Enumeration e = i.getOptions().elements(); e.hasMoreElements();) { IssueOption opt = (IssueOption)e.nextElement(); optionChoice.addItem(opt.shortDescription); } optionChoice.addActionListener(makeOptionChoiceListener(i)); optionChoice.setBackground(ViewColor.statusColor[i.getStatus()]); // Listen to any changes to the isssue i.addIssueListener (makeIssueListener(i, descriptionText, commentsText, priorityChoice, optionChoice)); // Add a new row // No actual row objects, though. /\/ issueToRowTable.put(i, descriptionText); rowToIssueTable.put(descriptionText, i); if (i.parent == null) { descriptionCol.add(descriptionText); commentsCol.add(commentsText); priorityCol.add(priorityChoice); optionCol.add(optionChoice); } else { JTextField parentText = (JTextField)issueToRowTable.get(i.parent); // Make parent description bold if it isn't already Font parentFont = parentText.getFont(); if (!parentFont.isBold()) { parentText.setFont(parentFont.deriveFont(Font.BOLD)); } // Add issue below parent's row and after any siblings // already present. We can assume that none of the siblings // have yet been expanded. /\/ int insertRow = descriptionCol.getRowIndex(parentText) + 1; int count = descriptionCol.getComponentCount(); while (insertRow < count) { JTextField text = (JTextField) descriptionCol.getComponent(insertRow); IdeelIssue next = (IdeelIssue)rowToIssueTable.get(text); if (next.parent == i.parent) insertRow++; else break; } descriptionCol.add(descriptionText, insertRow); commentsCol.add(commentsText, insertRow); priorityCol.add(priorityChoice, insertRow); optionCol.add(optionChoice, insertRow); } adjustSizes(); } protected void adjustSizes() { invalidate(); // Repack the root component now that the table's changed size. Component root = SwingUtilities.getRoot(this); if (root == null) { Debug.noteln("Tried to adjust sizes without root frame"); return; } JFrame r = (JFrame)root; // Resize only the first time ... // /\/: No, not even then // if (issueToRowTable.size() == 1) { // Dimension old_dim = r.getSize(); // Dimension new_dim = r.getPreferredSize(); // r.setSize(new Dimension((int)Math.max(new_dim.getWidth(), // old_dim.getWidth()), // (int)old_dim.getHeight())); // } r.validate(); } /* * Issue handled */ public void issueHandled(ControllerEvent e, IdeelIssue i, IssueHandler h) { // Called when an issue has been handled in some way other than // the user selecting an option from the menu - in particular when // the user has specified a manual expansion. The JComboBox's // listener will be told when we set the selected item and may // need to know this is not the usual case. At present, we let // it know by disabling the combobox. /\/ JComboBox optionChoice = findOptionChoice(i); String description = h.getActionDescription(); Debug.noteln("Issue " + i + " handled by " + description); // Disable before setting selected item. // See makeOptionChoiceListener. optionChoice.setEnabled(false); optionChoice.setSelectedItem(description); } JComboBox findOptionChoice(IdeelIssue i) { JTextField tf = (JTextField)issueToRowTable.get(i); int row = descriptionCol.getRowIndex(tf); return (JComboBox)optionCol.getComponent(row); } /* * New bindings */ public void newBindings(ControllerEvent event, Map bindings) { // We need to change the description of any issue that contains // one of the newly bound Variables in its pattern. // This should work but is far from ideal. /\/ Set vars = bindings.keySet(); for (Iterator i = issueToRowTable.entrySet().iterator(); i.hasNext();) { Map.Entry m = (Map.Entry)i.next(); IdeelIssue issue = (IdeelIssue)m.getKey(); JTextField text = (JTextField)m.getValue(); // Ought to have a proper check for whether any change is // needed, but this will do for now. /\/ if (!issue.patternVars.isEmpty()) { issue.shortDescription = Lisp.elementsToString(issue.pattern); text.setText(Util.repeatString(" ", issue.level) + issue.shortDescription); // Remove the "Bind variables" item from the option choice // if all variables are now bound? } } } /** * Returns a listener that can be called when the user selects an * issue option. */ ActionListener makeOptionChoiceListener(final IdeelIssue issue) { return new ActionListener() { JComboBox cb; public void actionPerformed(ActionEvent e) { cb = (JComboBox)e.getSource(); String optionName = (String)cb.getSelectedItem(); Debug.noteln("Selected option", optionName); IssueOption opt = findIssueOption(issue, optionName); Debug.assert(opt != null, "Can't find option", optionName); if (!cb.isEnabled()) { // See issueHandled method Debug.noteln("Assuming handled elsewhere"); return; } // The user can mess around with the options // while the status is BLANK but can't get // anything to happen unless the status is POSSIBLE. // /\/: This now depends on the issue and option classes. if (issue.actionCanBeTakenNow(opt)) // Test was: (issue.status == STATUS_POSSIBLE) handleIssue(opt); else { // Put selection back to "No Action" Debug.noteln("Ignoring option selection"); cb.setSelectedItem("No Action"); } } void handleIssue(IssueOption opt) { // Protect the GUI from problems elsewhere. try { issueManager.handleIssue(issue, opt); } catch (Exception e) { Debug.noteln("Exception while handling issue", issue); Debug.noteException(e); JOptionPane.showMessageDialog(IssueViewingTable.this, new Object[] {"Problem while handling issue", e.getMessage()}, "Problem while handling issue", JOptionPane.ERROR_MESSAGE); cb.setSelectedItem("No Action"); } } }; } /** * Returns a listener that can be called when the user selects * an issue priority. */ ActionListener makePriorityChoiceListener(final IdeelIssue issue) { return new ActionListener() { public void actionPerformed(ActionEvent e) { JComboBox cb = (JComboBox)e.getSource(); String priorityName = (String)cb.getSelectedItem(); Debug.noteln("Selected priority", priorityName); int priority = ViewColor.priorityValue(priorityName); issue.setPriority(priority); } }; } /** * Returns a listener than can be called when the user clicks * in the text of an issue description. */ MouseListener makeMouseListener(final IdeelIssue issue) { return new MouseAdapter() { public void mouseClicked(MouseEvent e) { JTextField clicked = (JTextField)e.getComponent(); Debug.noteln("User wants to look at issue", issue.shortDescription); ensureIssueEditor(); issueEditor.showIssue(issue); } }; } /** * Returns a listener that can be called when an issue changes * status or gets a new option, etc. */ IssueListener makeIssueListener(final IdeelIssue issue, final JTextField textField, final JTextField commentsField, final JComboBox priorityChoice, final JComboBox optionChoice) { // /\/: Names "...Field" to avoid confusion with the "...Text" ones? return new IssueListener() { public void statusChanged(IssueEvent e) { int status = issue.getStatus(); optionChoice.setBackground(ViewColor.statusColor[status]); if (status == STATUS_COMPLETE || status == STATUS_EXECUTING || status == STATUS_IMPOSSIBLE) { // /\/: Maybe remove all but selected item rather // than disable - so the colour will be seen. priorityChoice.setEnabled(false); optionChoice.setEnabled(false); } } public void priorityChanged(IssueEvent t) { int priority = issue.getPriority(); priorityChoice.setBackground (ViewColor.priorityColor[priority]); } public void newReport(IssueEvent e, Report report) { commentsField.setText(Util.firstLine(report.getText()).trim()); } public void issueEdited(IssueEvent e) { // Let's see ... what might have changed that we // care about? if (issue.getReports().isEmpty() && (!issue.comments.equals("") || !commentsField.getText().equals(""))) commentsField .setText(Util.firstLine(issue.comments).trim()); } public void newOption(IssueEvent e, IssueOption opt) { // Ignore the new option if the issue staus indicates // that it cannot be used. int status = issue.getStatus(); if (status == STATUS_COMPLETE || status == STATUS_EXECUTING || status == STATUS_IMPOSSIBLE) { return; } // Make sure the user can see which issue we're // talking about. Color savedBackground = textField.getBackground(); textField.setBackground(Color.blue); // See if we already know about an option with // the same description as the new one. String target = opt.shortDescription; String exists = null; for (int i = 0, len = optionChoice.getItemCount(); i < len; i++) { Object item = optionChoice.getItemAt(i); if (item.equals(target)) { exists = (String)item; break; } } if (exists != null) { // There is already an option with the same description. // See if the user wants the new option or the old one. // We assume that if we do nothing we will continue to // find the same option as before. if (shouldReplaceOption(exists)) { // The user wants the new option, so shadow the old. setShadowingOption(issue, opt.shortDescription, opt); Debug.assert(findIssueOption(issue, exists) == opt); } else { // Check that lookup doesn't get the new option. Debug.assert(findIssueOption(issue, exists) != opt); } } else { // We have a new option that does not match an // existing description, so add its description // to the JComboBox so the user can select it. JOptionPane.showMessageDialog(IssueViewingTable.this, new Object[] { "New option " + Util.quote(opt.shortDescription), "For issue " + Util.quote(issue.shortDescription) }, "New option", JOptionPane.INFORMATION_MESSAGE); optionChoice.addItem(opt.shortDescription); adjustSizes(); } // Restore issue colour changed to make sure the // user can see which issue we're talking about. textField.setBackground(savedBackground); } boolean shouldReplaceOption(String optDescription) { Object[] answers = { "Replace existing option", "Ignore new option"}; Object selected = JOptionPane.showInputDialog( IssueViewingTable.this, new Object[] { "Issue " + Util.quote(issue.shortDescription), "Already has an option " + Util.quote(optDescription) }, "New Option", // title JOptionPane.INFORMATION_MESSAGE, null, // icon answers, answers[0]); return selected.equals("Replace existing option"); } }; } /* * Testing */ public void addTestMenuItems(JMenu testMenu) { TestActionListener listener = new TestActionListener(); // testMenu.add(listener.makeMenuItem // ("Add example issues")); // testMenu.add(listener.makeMenuItem // ("Add issue \"Avoid elephants laki_safari_park\"")); } protected class TestActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); Debug.noteln("Issue viewer action:", command); if (command.equals("Add example issues")) { addExampleIssues(); } else if (command.equals ("Add issue \"Avoid elephants laki_safari_park\"")) { addIssue("avoid elephants laki_safari_park"); } else Debug.noteln("Nothing to do for", command); } public JMenuItem makeMenuItem(String text) { JMenuItem item = new JMenuItem(text); item.addActionListener(this); return item; } } public void addExampleIssues() { // addIssue("Note \"Observation 1 ...\""); // addIssue("Note \"Observation 2 ...\""); addIssue("Note \"Observation 23 Gao open-jeep N16.35 E35.28 ...\""); addIssue("Note \"Herd-at herd-3 N16.17 E35.04 ...\""); addIssue("Resolve conflict-between agent-3 agent-7 ..."); addIssue("Reallocate observe-herd herd-3 for agent-32 ..."); // invalidate(); } }