package context.arch.intelligibility.presenters;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import context.arch.comm.DataObject;
import context.arch.enactor.Enactor;
import context.arch.enactor.EnactorComponentInfo;
import context.arch.enactor.EnactorListener;
import context.arch.enactor.EnactorParameter;
import context.arch.intelligibility.DescriptiveExplainerDelegate;
import context.arch.intelligibility.query.AltQuery;
import context.arch.intelligibility.query.Query;
import context.arch.intelligibility.query.QueryListener;
import context.arch.intelligibility.reducers.ConjunctionReducer;
import context.arch.storage.Attributes;
/**
* <p>
* Utility class to obtain {@link Query} from a Swing user interface (JPanel).
* It allows the user to ask several questions of an output context:
* <ul>
* <li>Q_WHAT</li>
* <li>Q_WHEN</li>
* <li>Q_WHY</li>
* <li>Q_WHY_NOT</li>
* <li>Q_HOW_TO</li>
* <li>Q_OUTPUTS</li>
* <li>Q_INPUTS</li>
* <li>Q_WHAT_IF</li>
* </ul>
* </p>
* <p>
* These questions are selected via a combo box.
* The Why not and How To questions provide an extra combo box to choose the
* alternative outcome value to ask about. The What If question provides a
* panel to manipulate input values to ask about.
* </p>
* <p>
* Once the question is selected, this notifies its {@link QueryListener}s.
* Explanations are not generated here, but the listeners can take the passed
* Query to generate {@link Explanation}s using an {@link Explainer}.
* </p>
* <p>
* This class can be used as a reference implementation for how to create
* Queries in an application.
* </p>
* @author Brian Y. Lim
*
* @see QueryListener
* @see Query
* @see AltQuery
* @see WhatIfQuery
*/
public class QueryPanel extends JPanel implements ActionListener {
private static final long serialVersionUID = 7157745347576384770L;
public int Q_WHAT = 0;
public int Q_WHEN = 1;
public int Q_WHY = 3;
public int Q_WHY_NOT = 4;
public int Q_HOW_TO = 5;
public int Q_OUTPUTS = 7;
public int Q_INPUTS = 8;
public int Q_WHAT_IF = 9;
protected static String[] questions = {
"What", "When",
ComboBoxRenderer.SEPARATOR_MARKER,
"Why", "<html>Why isn't...</html>", "When would...",
ComboBoxRenderer.SEPARATOR_MARKER,
"What else", "What details", "What if..."
};
protected Vector<String> howToValues;
protected Vector<String> whyNotValues = new Vector<String>();
private String value;
protected String context;
protected String contextPretty;
private JLabel label1;
private JLabel label2;
private JComboBox questionCombo;
private JComboBox whyNotValuesCombo;
private JComboBox howToValuesCombo;
private Enactor enactor;
private WhatIfPanel whatIfPanel;
private JPanel wrapper;
private Query query;
protected DescriptiveExplainerDelegate descExplainer;
/**
* This should be called only after the enactor is properly started,
* or else there may be some corruption in the layout.
* @param enactor to associate questions with
* @param whatifReducer to reduce the inputs list for the What If UI.
* @param autoUpdate if true, then it will refresh its display whenever it detects a change in the enactor
*/
public QueryPanel(Enactor enactor, ConjunctionReducer whatifReducer, boolean autoUpdate) {
this.enactor = enactor;
descExplainer = enactor.getExplainer().getDescriptionExplainer();
this.context = enactor.getOutcomeName();
this.contextPretty = descExplainer.getPrettyName(enactor.getOutcomeName());
/*
* Layout
*/
this.setLayout(new BorderLayout());
this.setBorder(BorderFactory.createEtchedBorder());
wrapper = new JPanel();
wrapper.setLayout(new FlowLayout(FlowLayout.LEFT));
this.add(wrapper, BorderLayout.NORTH);
whatIfPanel = new WhatIfPanel(
context,
whatifReducer,
descExplainer,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
notifyListeners(getWhatIfQuery(System.currentTimeMillis()));
}
});
this.add(whatIfPanel, BorderLayout.CENTER);
update();
if (autoUpdate) {
enactor.addListener(new EnactorListener() {
@Override public void serviceExecuted(EnactorComponentInfo eci, String serviceName, String functionName, Attributes input, DataObject returnDataObject) {}
@Override public void parameterValueChanged(EnactorParameter parameter, Attributes validAtts, Object value) {}
@Override public void componentRemoved(EnactorComponentInfo eci, Attributes paramAtts) {}
@Override public void componentAdded(EnactorComponentInfo eci, Attributes paramAtts) {}
@Override
public void componentEvaluated(EnactorComponentInfo eci) {
update();
}
});
}
}
/**
* This should be called only after the enactor is properly started,
* or else there may be some corruption in the layout.
* @param enactor to associate questions with
* @param autoUpdate if true, then it will refresh its display whenever it detects a change in the enactor
*/
public QueryPanel(Enactor enactor, boolean autoUpdate) {
this(enactor, null, autoUpdate);
}
/**
* This should be called only after the enactor is properly started,
* or else there may be some corruption in the layout.
* This does not auto-update.
* @param enactor to associate questions with
*/
public QueryPanel(Enactor enactor) {
this(enactor, null);
}
/**
* This should be called only after the enactor is properly started,
* or else there may be some corruption in the layout.
* @param enactor to associate questions with
* @param whatifReducer to reduce the inputs list for the What If UI.
*/
public QueryPanel(Enactor enactor, ConjunctionReducer whatifReducer) {
this(enactor, whatifReducer, false);
}
/**
* If the panel was not set to auto-update, then this needs to be manually
* called whenever the enactor state changes.
*/
public void update() {
/*
* Reset
*/
wrapper.removeAll();
/*
* Extract output values
*/
value = enactor.getOutcomeValue();
howToValues = new Vector<String>(enactor.getOutcomeValues());
howToValues.add(0, "[select]"); // add empty as first
whyNotValues.clear();
for (String val : howToValues) {
if (!val.equals(value)) {
whyNotValues.add(val);
}
}
/*
* Components
*/
questionCombo = new JComboBox(questions);
questionCombo.setRenderer(new ComboBoxRenderer());
questionCombo.setMaximumRowCount(10); // so that we don't have to scroll
questionCombo.addActionListener(this);
wrapper.add(questionCombo);
label1 = new JLabel();
wrapper.add(label1);
whyNotValuesCombo = new JComboBox(whyNotValues);
whyNotValuesCombo.setRenderer(new ContextIcons.IconListCellRenderer());
whyNotValuesCombo.setVisible(false);
whyNotValuesCombo.addActionListener(this);
wrapper.add(whyNotValuesCombo);
howToValuesCombo = new JComboBox(howToValues);
howToValuesCombo.setRenderer(new ContextIcons.IconListCellRenderer());
howToValuesCombo.setVisible(false);
howToValuesCombo.addActionListener(this);
wrapper.add(howToValuesCombo);
label2 = new JLabel("?");
wrapper.add(label2);
whatIfPanel.setInputs(enactor.getExplainer().getInputsExplanation());
questionCombo.setSelectedIndex(0); // to invoke proper text rendering
//repaint();
invalidate();
}
/**
* Renderer for a JComboBox to add separators, if it detects a child as SEPARATOR_MARKER.
* @author Brian Y. Lim\
*/
protected class ComboBoxRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = -8082661052540808631L;
public static final String SEPARATOR_MARKER = "---";
public final JSeparator separator = new JSeparator(JSeparator.HORIZONTAL);
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value != null && value.equals(SEPARATOR_MARKER)) {
return separator;
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
/**
* To listen to actions performed on the combo boxes.
*/
@Override
public void actionPerformed(ActionEvent evt) {
Object src = evt.getSource();
if (src == questionCombo) {
int selectedIndex = questionCombo.getSelectedIndex();
questionSelected(selectedIndex);
}
else if (src == whyNotValuesCombo) {
questionSelected(Q_WHY_NOT);
}
else if (src == howToValuesCombo) {
questionSelected(Q_HOW_TO);
}
}
/**
* Called when a question is selected.
* @param selectedIndex the index of the question (Q_WHAT, Q_WHY, etc)
*/
protected void questionSelected(int selectedIndex) {
/*
* Change layout according to question type.
* Execute query if ready
*/
// reset
label1.setText("");
whyNotValuesCombo.setVisible(false);
howToValuesCombo.setVisible(false);
whatIfPanel.setVisible(false);
long timestamp = System.currentTimeMillis();
if (selectedIndex == Q_WHAT) {
label1.setText("is the " + contextPretty);
query = new Query(Query.QUESTION_WHAT, context, timestamp);
}
else if (selectedIndex == Q_WHEN) {
label1.setText("<html>did " + contextPretty + " become <i>" + value + "</i></html>");
query = new Query(Query.QUESTION_WHEN, context, timestamp);
}
else if (selectedIndex == Q_WHY) {
label1.setText("<html>is " + contextPretty + " <i>" + value + "</i></html>");
query = new Query(Query.QUESTION_WHY, context, timestamp);
}
else if (selectedIndex == Q_WHY_NOT) {
label1.setText(contextPretty);
whyNotValuesCombo.setVisible(true);
String altValue = whyNotValuesCombo.getSelectedItem().toString();
if (!altValue.equals("[select]")) {
query = new AltQuery(AltQuery.QUESTION_WHY_NOT, context, altValue, timestamp);
}
else {
// query = new Query(Query.QUESTION_NONE, context, timestamp);
query = new Query(null, context, timestamp);
}
}
else if (selectedIndex == Q_HOW_TO) {
label1.setText(contextPretty + " be");
howToValuesCombo.setVisible(true);
String altValue = howToValuesCombo.getSelectedItem().toString();
if (!altValue.equals("[select]")) {
query = new AltQuery(AltQuery.QUESTION_HOW_TO, context, altValue, timestamp);
}
else {
query = new Query(null, context, timestamp);
}
}
else if (selectedIndex == Q_OUTPUTS) {
label1.setText("can " + contextPretty + " be");
query = new Query(Query.QUESTION_OUTPUTS, context, timestamp);
}
else if (selectedIndex == Q_INPUTS) {
label1.setText("affect " + contextPretty);
query = new Query(Query.QUESTION_INPUTS, context, timestamp);
}
else if (selectedIndex == Q_WHAT_IF) {
label1.setText("conditions are different");
whatIfPanel.setVisible(true);
// send off void query to clear explanation
query = new Query(Query.QUESTION_NONE, context, timestamp);
}
if (query != null) {
// render(query, widgetState, value);
notifyListeners(query);
}
}
/**
* Can be overridden by subclasses to provide customized UI to get what-if input.
* @return
*/
public Query getWhatIfQuery(long timestamp) {
return whatIfPanel.getWhatIfQuery(timestamp);
}
/* -----------------------------------------------------------
* Query Listening code
* ----------------------------------------------------------- */
/** List of {@link QueryListener}s subscribed to this. */
protected List<QueryListener> listeners = new ArrayList<QueryListener>();
/**
* Add a {@link QueryListener} to be notified when the user asks a {@link Query}.
* @param listener
*/
public void addQueryListener(QueryListener listener) {
listeners.add(listener);
// notify if any query exists
if (query != null) {
notifyListeners(query);
}
}
/**
* Remove a {@link QueryListener}.
* @param listener
*/
public void removeQueryListener(QueryListener listener) {
listeners.remove(listener);
}
/**
* Called to notify {@link QueryListener}s that a new {@link Query}
* has been asked by the user.
* @param query
*/
protected void notifyListeners(Query query) {
for (QueryListener listener : listeners) {
listener.queryInvoked(query);
}
}
}