package context.arch.intelligibility.presenters; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.awt.event.MouseEvent; import java.text.DecimalFormat; import java.util.Enumeration; import java.util.EventObject; import java.util.List; import javax.swing.AbstractCellEditor; import javax.swing.DefaultListModel; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import context.arch.discoverer.ComponentDescription; import context.arch.enactor.Enactor; import context.arch.intelligibility.Explanation; import context.arch.intelligibility.expression.Comparison; import context.arch.intelligibility.expression.DNF; import context.arch.intelligibility.expression.Expression; import context.arch.intelligibility.expression.Negated; import context.arch.intelligibility.expression.Parameter; import context.arch.intelligibility.expression.Reason; import context.arch.intelligibility.query.Query; import context.arch.intelligibility.query.WhatIfQuery; import context.arch.storage.AttributeNameValue; /** * Renders explanations into a JPanel. * It only considers the structure of the explanation and not the context or question. * It requires supplied explanations to be in DNF. * @author Brian Y. Lim * */ public class TablePanelPresenter extends Presenter<JPanel> { protected JPanel panel; protected DecimalFormat nf = (DecimalFormat)DecimalFormat.getInstance(); protected Query query; /** * * @param parent that the rendered panel would be attached to */ public TablePanelPresenter(Enactor enactor) { super(enactor); panel = new JPanel(); panel.setLayout(new BorderLayout()); } public JPanel getPanel() { return panel; } /** * Each call to render really just updates the panel, but returns the same panel. */ @Override public JPanel render(Explanation explanation) { query = explanation.getQuery(); /* * Render content */ DNF content = explanation.getContent(); JComponent contentComponent = renderReasons(content); panel.removeAll(); panel.add(contentComponent); panel.revalidate(); return panel; } /* ------------------------------------------------------ * Rendering for explanation query * ------------------------------------------------------ */ // Not doing /* ------------------------------------------------------ * Rendering for explanation content * ------------------------------------------------------ */ /** * Renders an expression that is terminal, i.e. not Conjunction or Disjunction * @param expression * @return */ protected JComponent renderTerminal(Parameter<?> expression) { /* * The following allows multiple columns in a table, rather than a single text-field. */ Reason reason = new Reason(); AbstractTableModel tableModel; boolean whatif = false; // if (query != null && query.getQuestion().equals(WhatIfQuery.QUESTION_WHAT_IF)) { if (query != null && query.getQuestion().equals(WhatIfQuery.QUESTION_INPUTS)) { whatif = true; } if (expression instanceof Comparison<?>) { reason.add(expression); // tableModel = new ComparisonsTableModel(expressionList); tableModel = new DualComparisonsTableModel(reason); } else if (expression instanceof Parameter<?>) { reason.add(expression); tableModel = new ParametersTableModel(reason, whatif); } else { return null; } ExplanationTable table = new ExplanationTable(tableModel); if (whatif) { TableColumn col = table.getColumnModel().getColumn(1); col.setCellEditor(table.new SpinnerEditor()); } JScrollPane scrollpane = new JScrollPane(table); return scrollpane; } /** * * @param reason * @return the scroll pane containing the JList */ protected JComponent renderReason(Reason reason) { if (reason.isEmpty()) { return null; } AbstractTableModel tableModel; boolean whatif = false; // if (query != null && query.getQuestion().equals(WhatIfQuery.QUESTION_WHAT_IF)) { if (query != null && query.getQuestion().equals(WhatIfQuery.QUESTION_INPUTS)) { whatif = true; } /* * Check type */ Expression exp0 = reason.get(0); if (exp0 instanceof Comparison<?>) { // tableModel = new ComparisonsTableModel(expressionList); tableModel = new DualComparisonsTableModel(reason); } else if (exp0 instanceof Parameter<?>) { tableModel = new ParametersTableModel(reason, whatif); } else { return null; } JTable table = new ExplanationTable(tableModel); JScrollPane scrollpane = new JScrollPane(table); return scrollpane; } protected void addTerminalToList(Parameter<?> expression, DefaultListModel listModel) { if (expression instanceof Negated<?>) {} else if (expression instanceof Comparison<?>) {} else if (expression instanceof Parameter<?>) {} else { return; // do nothing if not terminal } listModel.addElement(renderTerminal(expression)); } /** * E.g. for multiple traces of Why Not and How To. * @param disjunction */ protected JComponent renderReasons(DNF disjunction) { if (disjunction.size() == 1) { // if it's just size one // then reduce to simply a conjunction return renderReason(disjunction.get(0)); } // to put lists in JTabbedPane tabbedPane = new JTabbedPane(); int i = 0; for (Reason reason : disjunction) { JComponent trace = renderReason(reason); tabbedPane.addTab("Reason " + ++i, trace); } return tabbedPane; } /** * Subclasses should override this to render the expression as pretty strings. * @param expression * @return */ protected static String expressionToPrettyString(Expression expression) { return expression.toString(); // TODO: this should be offloaded to a StringPresenter class } /* ------------------------------------------------------ * Classes to model explanations * ------------------------------------------------------ */ protected class ExplanationTable extends JTable { private static final long serialVersionUID = -8637900302897337738L; protected ExplanationTable(AbstractTableModel model) { super(model); // can only select one cell at a time setSelectionMode(ListSelectionModel.SINGLE_SELECTION); setCellSelectionEnabled(true); /* * Enhance with whatever functionality that subclass presenters may add */ enhanceTable(this); /* * Adjust column widths */ Enumeration<TableColumn> cols = getColumnModel().getColumns();; while (cols.hasMoreElements()) { TableColumn col = cols.nextElement(); if (col.getHeaderValue().equals("Comparison")) { col.setPreferredWidth(10); } } if (getColumnModel().getColumnCount() > 1) { TableColumn col = getColumnModel().getColumn(1); col.setCellRenderer(new DefaultTableCellRenderer() { private static final long serialVersionUID = 1L; @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (!(value instanceof Double)) { return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } final double val = (Double)value; nf.applyPattern("#0.00"); JLabel comp = new JLabel("" + nf.format(val)) { private static final long serialVersionUID = 4626767143154932272L; private Color POS_COLOR = new Color(30,100,255, 180); private Color NEG_COLOR = new Color(245,40,30, 180); private Color LINE_COLOR = new Color(120,120,120, 200); @Override public void paint(Graphics g) { int W = getWidth() / 2; int H = getHeight(); int w = (int)(W*val/50); if (val > 0) { g.setColor(POS_COLOR); g.fillRect(W, 0, w, H); g.setColor(LINE_COLOR); g.drawRect(W, 0, w, H); } else { g.setColor(NEG_COLOR); g.fillRect(W+w, 0, -w, H-1); g.setColor(LINE_COLOR); g.drawRect(W+w, 0, -w, H-1); } super.paint(g); } }; comp.setHorizontalTextPosition(JLabel.CENTER); // this somehow doesn't work! return comp; } }); } } /** * Allows cell in JTable to be editable when double clicked. * Ref: http://www.exampledepot.com/egs/javax.swing.table/Spinner.html */ public class SpinnerEditor extends AbstractCellEditor implements TableCellEditor, ChangeListener { private static final long serialVersionUID = -8330020225607069931L; final JSpinner spinner = new JSpinner(); // Initializes the spinner. private JTable table; private int row = -1; public SpinnerEditor() { SpinnerModel model = new SpinnerNumberModel(0, //initial value -100, //min 100, //max .2); //step spinner.setModel(model); spinner.addChangeListener(this); } // Prepares the spinner component and returns it. public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { spinner.setValue(value); this.table = table; this.row = row; return spinner; } // Enables the editor only for double-clicks. public boolean isCellEditable(EventObject evt) { if (evt instanceof MouseEvent) { return ((MouseEvent)evt).getClickCount() >= 2; } return true; } // Returns the spinners current value. public Object getCellEditorValue() { return spinner.getValue(); } @Override public void stateChanged(ChangeEvent evt) { if (table != null && row > -1) { int column = 1; this.table.getModel().setValueAt(spinner.getValue(), row, column); } } } @Override public Component prepareRenderer(TableCellRenderer renderer, int rowIndex, int vColIndex) { /* * Show text of cell in tooltip, since cell may not be big enough to show everything */ Component c = super.prepareRenderer(renderer, rowIndex, vColIndex); if (c instanceof JComponent) { JComponent jc = (JComponent)c; Object value = this.getValueAt(rowIndex, vColIndex); if (value != null) { jc.setToolTipText(value.toString()); } } if (c instanceof JLabel) { if (getColumnName(vColIndex).equals("Comparison")) { ((JLabel)c).setHorizontalAlignment(SwingConstants.CENTER); } } return c; } } /** * This assumes that most explanations are rendered in tables, and specifically in table cells. * The default behavior is to add tooltips to the cells. * * Subclasses can enhance further, e.g. by supporting right-click popups to ask for explanations. * * @param comp the table cell component to enhance * @param table * @param rowIndex * @param vColIndex */ protected void enhanceTable(ExplanationTable table) { // do nothing for now } protected static class ValuesTableModel extends AbstractTableModel { private static final long serialVersionUID = 3689437044797418681L; protected Comparable<?>[] values; protected int numCols; protected ValuesTableModel() {} protected ValuesTableModel(List<Expression> list) { values = new Comparable<?>[list.size()]; // numCols = 1; // // for (int i = 0; i < values.length; i++) { // Value<?> val = (Value<?>)list.get(i); // values[i] = val.getValue(); // } } @Override public String getColumnName(int col) { switch (col) { case 0: return "Value"; default: return null; } } @Override public Object getValueAt(int row, int col) { switch (col) { case 0: return values[row]; default: return null; } } @Override public boolean isCellEditable(int row, int col) { return false; } @Override public int getRowCount() { return values.length; } @Override public int getColumnCount() { return numCols; } } protected class ParametersTableModel extends ValuesTableModel { private static final long serialVersionUID = 5755342189654583969L; protected String[] contexts; // name private boolean editable; protected ParametersTableModel(Reason list, boolean editable) { super(); this.editable = editable; contexts = new String[list.size()]; values = new Comparable<?>[list.size()]; numCols = 2; for (int i = 0; i < contexts.length; i++) { Parameter<?> param = (Parameter<?>)list.get(i); contexts[i] = param.getName(); values[i] = param.getValue(); } } @Override public String getColumnName(int col) { switch (col) { case 0: return "Context"; case 1: return "Value"; default: return null; } } @Override public Object getValueAt(int row, int col) { switch (col) { case 0: return contexts[row]; case 1: return values[row]; default: return null; } } @Override public void setValueAt(Object value, int row, int col) { switch (col) { case 0: contexts[row] = (String)value; case 1: values[row] = (Comparable<?>) value; } } @Override public boolean isCellEditable(int row, int col) { if (editable && col == 1) { return true; } return false; } /** * TODO This actually overrides the original widget state after a What If query. * We can then subsequently ask other types of questions of this new state. * @return */ @SuppressWarnings("unchecked") public <T extends Comparable<? super T>> ComponentDescription saveWidgetState() { ComponentDescription widgetState = TablePanelPresenter.this.enactor.getInWidgetState(); for (int r = 0; r < contexts.length; r++) { widgetState.getNonConstantAttributes().add(AttributeNameValue.instance(contexts[r], (T) values[r])); } return widgetState; } } protected class ComparisonsTableModel extends ParametersTableModel { private static final long serialVersionUID = -1561280486178631629L; protected Comparison.Relation[] comparisons; protected ComparisonsTableModel(Reason list) { super(list, false); comparisons = new Comparison.Relation[list.size()]; numCols = 3; for (int i = 0; i < contexts.length; i++) { Comparison<?> comp = (Comparison<?>)list.get(i); comparisons[i] = comp.getRelationship(); } } @Override public String getColumnName(int col) { switch (col) { case 0: return "Context"; case 1: return "Comparison"; case 2: return "Value"; default: return null; } } @Override public Object getValueAt(int row, int col) { switch (col) { case 0: return contexts[row]; case 1: return comparisons[row]; case 2: return values[row]; default: return null; } } } protected class DualComparisonsTableModel extends ComparisonsTableModel { private static final long serialVersionUID = 2715542289639911385L; protected Object[] values1; protected Comparison.Relation[] comparisons1; protected DualComparisonsTableModel(Reason list) { super(list); comparisons1 = new Comparison.Relation[list.size()]; values1 = new Object[list.size()]; numCols = 5; for (int i = 0; i < contexts.length; i++) { Comparison<?> comp = (Comparison<?>)list.get(i); comparisons1[i] = comp.getRelationship1(); if (comparisons1[i] == null) { // not set comparisons1[i] = Comparison.Relation.NO_RELATION; // TODO set non-null to prevent null pointer exception values1[i] = ""; } else { values1[i] = comp.getValue1(); } } } @Override public String getColumnName(int col) { switch (col) { case 0: return "Lower Value"; case 1: return "Comparison"; case 2: return "Context"; case 3: return "Comparison"; case 4: return "Upper Value"; default: return null; } } @Override public Object getValueAt(int row, int col) { switch (col) { case 0: return values1[row]; case 1: return comparisons1[row]; case 2: return contexts[row]; case 3: return comparisons[row]; case 4: return values[row]; default: return null; } } } /** * Convenience method to wrap a JComponent in a scrollbar * @param component * @return */ public static JScrollPane scrollbarWrap(JComponent component) { // wrap so that it has a flow layout JPanel wrapper = new JPanel(); wrapper.add(component); // so that it can scroll JScrollPane scrollpane = new JScrollPane(component); scrollpane.getVerticalScrollBar().setUnitIncrement(16); // doesn't work since no size has been determined yet // scrollpane.getViewport().setViewPosition(new Point(0,0)); //// wrapper.scrollRectToVisible(new Rectangle(0,0,0,0)); // scrollpane.revalidate(); // System.out.println(scrollpane.getViewport().getViewRect()); // System.out.println(scrollpane.getViewport().getViewPosition()); return scrollpane; } }