/* * This file is part of the OSMembrane project. * More informations under www.osmembrane.de * * The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0. * for more details about the license see http://www.osmembrane.de/license/ * * Source: $HeadURL$ ($Revision$) * Last changed: $Date$ */ package de.osmembrane.view.panels; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Observable; import java.util.Observer; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.DefaultCellEditor; import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.JToggleButton.ToggleButtonModel; import javax.swing.SwingConstants; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellEditor; import de.osmembrane.Application; import de.osmembrane.controller.ActionRegistry; import de.osmembrane.controller.actions.EditBoundingBoxPropertyAction; import de.osmembrane.controller.actions.EditDirectoryPropertyAction; import de.osmembrane.controller.actions.EditFilePropertyAction; import de.osmembrane.controller.actions.EditListPropertyAction; import de.osmembrane.controller.actions.EditPropertyAction; import de.osmembrane.controller.actions.LoadFunctionPresetAction; import de.osmembrane.controller.actions.SaveFunctionPresetAction; import de.osmembrane.controller.events.ContainingEvent; import de.osmembrane.controller.events.ContainingFunctionChangeParameterEvent; import de.osmembrane.exceptions.ControlledException; import de.osmembrane.exceptions.ExceptionSeverity; import de.osmembrane.model.pipeline.AbstractFunction; import de.osmembrane.model.pipeline.AbstractParameter; import de.osmembrane.model.pipeline.AbstractTask; import de.osmembrane.model.pipeline.PipelineObserverObject; import de.osmembrane.resources.Resource; import de.osmembrane.tools.I18N; import de.osmembrane.tools.IconLoader.Size; import de.osmembrane.view.ViewRegistry; import de.osmembrane.view.components.JRowTable; import de.osmembrane.view.components.JTextFieldWithButton; import de.osmembrane.view.components.JTextFieldWithButtonCellEditor; import de.osmembrane.view.components.RowEditorModel; /** * The inspector panel component to realize the function inspector. * * @see "Spezifikation.pdf, chapter 2.1.6 (German)" * * @author tobias_kuhn * */ public class InspectorPanel extends JPanel implements Observer { private static final long serialVersionUID = -4331036066478472018L; /** * the {@link JLabel} that shows the function name */ private JLabel functionName; /* * the JRowTable that displays the data of the function along with its * InspectorPanelTableModel and RowEditorModel. */ private JRowTable propertyTable; private InspectorPanelTableModel propertyTableModel; private RowEditorModel rowEditorModel; /** * the InspectorPanelTableTaskComboBoxModel for the combo box to choose the * tasks */ private InspectorPanelTableTaskComboBoxModel taskComboModel; /** * the {@link JPanel} and {@link JLabel} that display the context-sensitive * help */ private JPanel hint; private JEditorPane hintLabel; /** * Useful {@link Color} definitions */ private static final Color LIGHT_BLUE = new Color(0.9f, 0.9f, 1.0f); // private static final Color LIGHTER_BLUE = new Color(0.95f, 0.95f, 1.0f); private static final Color LIGHT_WHITE = new Color(0.95f, 0.95f, 0.95f); private static final Color LIGHTER_WHITE = new Color(1.0f, 1.0f, 1.0f); private static final Color LIGHT_YELLOW = new Color(1.0f, 1.0f, 0.9f); /** * An empty hint that is actually valid. (The hint panel normally checks for * == null and .isEmpty()) */ protected static final String VALID_EMPTY_HINT = " "; /** * Text to be shown on "edit me further" buttons */ private static final String EDIT_BUTTON_CAPTION = "..."; /** * the reference to the {@link AbstractFunction} which is currently * inspected */ private AbstractFunction inspecting = null; /** * Buttons to load and save FunctionPresets/defaults. */ private JButton savePreset; private JButton loadPreset; private JButton setDefault; /** * Initializes the {@link InspectorPanel} and display */ public InspectorPanel() { // register as observer ViewRegistry.getInstance().addObserver(this); // caption functionName = new JLabel(I18N.getInstance().getString( "View.Inspector.NoSelection")); functionName.setFont(functionName.getFont().deriveFont(Font.BOLD, 1.2f * functionName.getFont().getSize())); functionName.setHorizontalAlignment(SwingConstants.CENTER); JPanel functionCaption = new JPanel(); functionCaption.setLayout(new BorderLayout()); functionCaption.add(functionName, BorderLayout.CENTER); JPanel buttons = new JPanel(); buttons.setLayout(new GridLayout(1, 2)); setDefault = new JButton(Resource.PROGRAM_ICON.getImageIcon("undo.png", Size.SMALL)); setDefault.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { setDefaults(); } }); buttons.add(setDefault); loadPreset = new JButton(ActionRegistry.getInstance().get( LoadFunctionPresetAction.class)); buttons.add(loadPreset); savePreset = new JButton(ActionRegistry.getInstance().get( SaveFunctionPresetAction.class)); buttons.add(savePreset); functionCaption.add(buttons, BorderLayout.EAST); // display taskComboModel = new InspectorPanelTableTaskComboBoxModel(); rowEditorModel = new RowEditorModel(); propertyTableModel = new InspectorPanelTableModel(); propertyTable = new JRowTable(propertyTableModel, rowEditorModel); propertyTable.addMouseListener(new MouseListener() { @Override public void mousePressed(MouseEvent e) { } @Override public void mouseClicked(MouseEvent e) { // ensure clicking results in an edit int row = propertyTable.rowAtPoint(e.getPoint()); propertyTable.editCellAt(row, 1); } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } }); propertyTable.addMouseMotionListener(new MouseMotionListener() { @Override public void mouseMoved(MouseEvent e) { // show applicable hint if (inspecting != null) { int row = propertyTable.rowAtPoint(e.getPoint()); if (row == -1) { setHintText(inspecting.getDescription()); } else if (row == 0) { setHintText(inspecting.getActiveTask().getDescription()); } else { if (row > inspecting.getActiveTask().getParameters().length) { Application .handleException(new ControlledException( this, ExceptionSeverity.UNEXPECTED_BEHAVIOR, I18N.getInstance() .getString( "View.Inspector.ParamCountException"))); } setHintText(inspecting.getActiveTask().getParameters()[row - 1] .getDescription()); } } /* inspecting != null */ } @Override public void mouseDragged(MouseEvent e) { } }); propertyTable.setDefaultRenderer(JComponent.class, new InspectorPanelTableRenderer()); // for nicer ComboBoxes propertyTable.setRowHeight((int) (1.2 * propertyTable.getRowHeight())); // hint hint = new JPanel(); hint.setBackground(InspectorPanel.LIGHT_YELLOW); hint.setLayout(new GridLayout(1, 1)); hintLabel = new JEditorPane(); hintLabel.setEditable(false); hintLabel.setOpaque(false); hintLabel.setBorder(null); hintLabel.setContentType("text/html"); hintLabel.setAlignmentX(Component.CENTER_ALIGNMENT); hint.add(hintLabel); // align this to look good setLayout(new BorderLayout()); add(functionCaption, BorderLayout.NORTH); JSplitPane propertiesAndHints = new JSplitPane( JSplitPane.VERTICAL_SPLIT, true, new JScrollPane(propertyTable), hint); add(propertiesAndHints, BorderLayout.CENTER); } /** * Sets all changes in the currently inspected function *in the currently * set task* back to their default values. */ protected void setDefaults() { if (inspecting != null) { for (AbstractParameter ap : inspecting.getActiveTask() .getParameters()) { ap.setValue(ap.getDefaultValue()); } } } /** * Sets the hint text * * @param hintText * the hintText to set */ protected void setHintText(String hintText) { if ((hintText == null) || (hintText.isEmpty())) { hintText = I18N.getInstance().getString("View.Inspector.NoHint"); } StringBuilder sb = new StringBuilder(); FontMetrics fm = hintLabel.getFontMetrics(hintLabel.getFont()); int lineWidth = 0; for (String word : hintText.split("\\s")) { int thisWidth = fm.stringWidth(word + " "); // if this line is wider than possible, make a new line if (lineWidth + thisWidth >= hint.getWidth() - 8) { sb.append("<br />" + word + " "); lineWidth = 0; } else { // append lineWidth += thisWidth; sb.append(word + " "); } } hintLabel.setText("<html><center><br /><p>" + sb.toString() + "</p></center></html>"); } @Override public void update(Observable o, Object arg) { if ((inspecting != null) && (arg instanceof PipelineObserverObject)) { PipelineObserverObject poo = (PipelineObserverObject) arg; switch (poo.getType()) { case ADD_FUNCTION: case CHANGE_FUNCTION: if (poo.getChangedFunction().equals(inspecting)) { inspect(inspecting); repaint(); } break; case DELETE_FUNCTION: if (!poo.getChangedFunction().equals(inspecting)) { break; } case FULLCHANGE: inspect(null); break; } } } /** * @param inspect * the inspected function to set */ public void inspect(AbstractFunction inspect) { rowEditorModel.clear(); TableCellEditor tce = propertyTable.getCellEditor(); if (tce != null) { Object value = tce.getCellEditorValue(); int editRow = propertyTable.getEditingRow(); int editColumn = propertyTable.getEditingColumn(); // required to prevent inspect -> setValue -> ... loop propertyTable.removeEditor(); // change iff value changed // + work-around for model code if ((editRow > 0) && (inspecting != null)) { String realValue = inspecting.getActiveTask().getParameters()[editRow - 1] .getValue(); if (realValue == null) { realValue = inspecting.getActiveTask().getParameters()[editRow - 1] .getDefaultValue(); } if (realValue == null) { realValue = ""; } if ((value != null) && (!value.equals(realValue))) { propertyTable.setValueAt(value, editRow, editColumn); } } } propertyTable.removeEditor(); this.inspecting = inspect; if (inspect == null) { functionName.setText(I18N.getInstance().getString( "View.Inspector.NoSelection")); } else { functionName.setText(inspect.getFriendlyName()); rowEditorModel.setEditorRow(0, new DefaultCellEditor(new JComboBox( taskComboModel))); // find the appropriate RowEditors for the parameters for (int i = 0; i < inspect.getActiveTask().getParameters().length; i++) { final AbstractParameter ap = inspect.getActiveTask() .getParameters()[i]; String realValue = (ap.getValue() != null) ? ap.getValue() : ap .getDefaultValue(); switch (ap.getType()) { case ENUM: // use ComboBox with enum values InspectorPanelTableCustomEnumComboBoxModel iptcecbm = new InspectorPanelTableCustomEnumComboBoxModel( ap); DefaultCellEditor dceEnum = new DefaultCellEditor( new JComboBox(iptcecbm)); rowEditorModel.setEditorRow(i + 1, dceEnum); break; case BOOLEAN: // use CheckBox with true/false JCheckBox jcb = new JCheckBox(); jcb.setModel(new InspectorPanelTableBooleanCheckBoxModel(ap)); jcb.setSelected(realValue.equals(Boolean.TRUE.toString())); DefaultCellEditor dceBoolean = new DefaultCellEditor(jcb); rowEditorModel.setEditorRow(i + 1, dceBoolean); break; case BBOX: // use JTFWB with EditBBAction JTextFieldWithButton jtfwbBB = new JTextFieldWithButton( realValue, EDIT_BUTTON_CAPTION); jtfwbBB.fieldNoBorders(); // jtfwbBB.setValue("(Bounding Box)", false); jtfwbBB.addButtonActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { propertyTable.removeEditor(); Action aBB = ActionRegistry.getInstance().get( EditBoundingBoxPropertyAction.class); ContainingEvent ceBB = new ContainingEvent(this, ap); aBB.actionPerformed(ceBB); } }); rowEditorModel.setEditorRow(i + 1, new JTextFieldWithButtonCellEditor(jtfwbBB)); break; case LIST: // use JTFWB with EditListAction JTextFieldWithButton jtwbList = new JTextFieldWithButton( realValue, EDIT_BUTTON_CAPTION); jtwbList.fieldNoBorders(); jtwbList.addButtonActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { propertyTable.removeEditor(); Action aList = ActionRegistry.getInstance().get( EditListPropertyAction.class); ContainingEvent ceList = new ContainingEvent(this, ap); aList.actionPerformed(ceList); } }); rowEditorModel.setEditorRow(i + 1, new JTextFieldWithButtonCellEditor(jtwbList)); break; case FILENAME: // use JTFWB with EditFileAction JTextFieldWithButton jtwbFile = new JTextFieldWithButton( realValue, EDIT_BUTTON_CAPTION); jtwbFile.fieldNoBorders(); jtwbFile.addButtonActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { propertyTable.removeEditor(); Action aList = ActionRegistry.getInstance().get( EditFilePropertyAction.class); ContainingEvent ceFile = new ContainingEvent(this, ap); aList.actionPerformed(ceFile); } }); rowEditorModel.setEditorRow(i + 1, new JTextFieldWithButtonCellEditor(jtwbFile)); break; case DIRECTORY: // use JTFWB with EditDirectoryAction JTextFieldWithButton jtwbDir = new JTextFieldWithButton( realValue, EDIT_BUTTON_CAPTION); jtwbDir.fieldNoBorders(); jtwbDir.addButtonActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { propertyTable.removeEditor(); Action aList = ActionRegistry.getInstance().get( EditDirectoryPropertyAction.class); ContainingEvent ceDir = new ContainingEvent(this, ap); aList.actionPerformed(ceDir); } }); rowEditorModel.setEditorRow(i + 1, new JTextFieldWithButtonCellEditor(jtwbDir)); break; default: // use a plain string JTextField JTextField textField = new JTextField(realValue); textField.setOpaque(true); textField.setBorder(null); DefaultCellEditor dce = new DefaultCellEditor(textField); dce.setClickCountToStart(1); rowEditorModel.setEditorRow(i + 1, dce); } } /* for */ } setDefault.setEnabled((inspect != null)); loadPreset.setEnabled((inspect != null)); savePreset.setEnabled((inspect != null)); propertyTableModel.fireTableDataChanged(); } /** * The table model of the display table of the {@link InspectorPanel}. * * @author tobias_kuhn * */ class InspectorPanelTableModel extends DefaultTableModel { private static final long serialVersionUID = 6495527693821933683L; private String[] columns = new String[2]; /** * Creates a new {@link InspectorPanelTableModel} */ public InspectorPanelTableModel() { columns[0] = I18N.getInstance().getString("View.Parameter"); columns[1] = I18N.getInstance().getString("View.Value"); } @Override public int getColumnCount() { return 2; } @Override public int getRowCount() { if (inspecting == null) { return 0; } else { return 1 + inspecting.getActiveTask().getParameters().length; } } @Override public boolean isCellEditable(int row, int column) { return (column == 1); } @Override public String getColumnName(int column) { return columns[column]; } @Override public Class<?> getColumnClass(int columnIndex) { return JComponent.class; } @Override public Object getValueAt(int row, int column) { if (inspecting == null) { return null; } else { Object result = null; switch (column) { case 0: // property descriptions if (row > 0) { result = inspecting.getActiveTask().getParameters()[row - 1] .getFriendlyName(); } else { result = I18N.getInstance().getString("View.Task"); } break; default: // property values TableCellEditor tce = rowEditorModel.getEditorRow(row); if (tce != null) { boolean selected = (propertyTable.getSelectedRow() == row) && (propertyTable.getSelectedColumn() == column); result = tce .getTableCellEditorComponent(propertyTable, tce.getCellEditorValue(), selected, row, column); } else { result = new String(); } if (result instanceof JTextField) { result = ((JTextField) result).getText(); } } return result; } /* else */ } @Override public void setValueAt(Object aValue, int row, int column) { if (inspecting != null) { ContainingFunctionChangeParameterEvent cfcpe = new ContainingFunctionChangeParameterEvent( this, inspecting); if ((row > 0) && (aValue instanceof String)) { cfcpe.setChangedParameter(inspecting.getActiveTask() .getParameters()[row - 1]); cfcpe.setNewParameterValue(aValue.toString()); ActionRegistry.getInstance().get(EditPropertyAction.class) .actionPerformed(cfcpe); } } } } /* InspectorPanelTableModel */ /** * Model for the task's combo box of the {@link InspectorPanel}'s table. Is * in a contest with {@link ContainingFunctionChangeParameterEvent} and * {@link InspectorPanelTableCustomEnumComboBoxModel} for the longest name * in the project. * * @author tobias_kuhn * */ class InspectorPanelTableTaskComboBoxModel extends DefaultComboBoxModel { private static final long serialVersionUID = 5611487798519928339L; @Override public int getSize() { if (inspecting == null) { return 0; } else { return inspecting.getAvailableTasks().length; } } @Override public Object getElementAt(int index) { if (inspecting == null) { return null; } else { return inspecting.getAvailableTasks()[index].getFriendlyName(); } } @Override public Object getSelectedItem() { if (inspecting == null) { return null; } else { // here we pray, Java uses .equals() return inspecting.getActiveTask().getFriendlyName(); } } @Override public void setSelectedItem(Object anObject) { if ((inspecting != null) && (anObject != null)) { ContainingFunctionChangeParameterEvent cfcpe = new ContainingFunctionChangeParameterEvent( this, inspecting); for (AbstractTask at : inspecting.getAvailableTasks()) { if (anObject.equals(at.getFriendlyName()) && !at.equals(inspecting.getActiveTask())) { cfcpe.setNewTask(at); ActionRegistry.getInstance() .get(EditPropertyAction.class) .actionPerformed(cfcpe); break; } } /* for tasks */ } } } /* InspectorPanelTableTaskComboBoxModel */ /** * Model for an enum property of the combo box of the {@link InspectorPanel} * 's table. Is in a contest with * {@link ContainingFunctionChangeParameterEvent} and * {@link InspectorPanelTableTaskComboBoxModel} for the longest name in the * project. */ class InspectorPanelTableCustomEnumComboBoxModel extends DefaultComboBoxModel { private static final long serialVersionUID = 5695998035505672596L; /** * the parameter with enum to be edited */ private AbstractParameter param; /** * Creates a new {@link InspectorPanelTableCustomEnumComboBoxModel} * * @param param * the enum parameter to be edited */ public InspectorPanelTableCustomEnumComboBoxModel( AbstractParameter param) { this.param = param; } @Override public int getSize() { return param.getEnumValue().length; } @Override public Object getElementAt(int index) { return param.getEnumValue()[index].getValue(); } @Override public Object getSelectedItem() { String realValue = (param.getValue() != null) ? param.getValue() : param.getDefaultValue(); return realValue; } @Override public void setSelectedItem(Object anObject) { if ((anObject != null) && (anObject instanceof String)) { ContainingFunctionChangeParameterEvent cfcpe = new ContainingFunctionChangeParameterEvent( this, inspecting); cfcpe.setChangedParameter(param); cfcpe.setNewParameterValue((String) anObject); ActionRegistry.getInstance().get(EditPropertyAction.class) .actionPerformed(cfcpe); } } } /* InspectorPanelTableCustomEnumComboBoxModel */ /** * /** Model for a boolean property of the check box of the * {@link InspectorPanel} 's table. I suppose by now, you know these * abominations. * * @author tobias_kuhn * */ class InspectorPanelTableBooleanCheckBoxModel extends ToggleButtonModel { private static final long serialVersionUID = -3716607547245684756L; /** * The param with boolean to be edited */ private AbstractParameter param; /** * The last edit action in {@link System#currentTimeMillis()}. */ private long lastSetFire; /** * Creates a new {@link InspectorPanelTableBooleanCheckBoxModel} * * @param param * the boolean parameter to be edited */ public InspectorPanelTableBooleanCheckBoxModel(AbstractParameter param) { this.param = param; this.lastSetFire = System.currentTimeMillis(); } @Override public boolean isSelected() { String realValue = (param.getValue() != null) ? param.getValue() : param.getDefaultValue(); return realValue.equals(Boolean.TRUE.toString()); } @Override public void setSelected(boolean b) { /* * this method fires way too often, so we got to make sure, it does * not constantly recall itself. (= 50msec blocking) */ long now = System.currentTimeMillis(); if (now - lastSetFire < 50) { return; } lastSetFire = System.currentTimeMillis(); /* * additionally we only want swaps to get to the model. Ask the damn * Swing coders why the fork this thing is so messed up. */ Boolean bool = Boolean.valueOf(b); String realValue = (param.getValue() != null) ? param.getValue() : param.getDefaultValue(); if (realValue.equals(bool.toString())) { return; } ContainingFunctionChangeParameterEvent cfcpe = new ContainingFunctionChangeParameterEvent( this, inspecting); cfcpe.setChangedParameter(param); cfcpe.setNewParameterValue(bool.toString()); ActionRegistry.getInstance().get(EditPropertyAction.class) .actionPerformed(cfcpe); } } /* InspectorPanelTableBooleanCheckBoxModel */ /** * The custom cell renderer for the display table of the * {@link InspectorPanel}. * * @author tobias_kuhn * */ class InspectorPanelTableRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = -8005963595998602494L; @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c; c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); // fetch correct components if (value instanceof JComboBox) { c = (Component) value; } else if (value instanceof JTextFieldWithButton) { c = (Component) value; } else if (value instanceof JTextField) { c = (Component) value; } else if (value instanceof JCheckBox) { c = (Component) value; } // some nice colors for every other row there if (column == 0) { c.setBackground((row % 2 == 0) ? InspectorPanel.LIGHT_BLUE : InspectorPanel.LIGHT_BLUE); } else { c.setBackground((row % 2 == 0) ? InspectorPanel.LIGHT_WHITE : InspectorPanel.LIGHTER_WHITE); } c.setForeground(Color.BLACK); // if it's a required parameter, print its descriptor blue if ((column == 0) && (row > 0)) { if (inspecting.getActiveTask().getParameters()[row - 1] .isRequired()) { c.setFont(c.getFont().deriveFont(Font.BOLD)); } else { c.setFont(c.getFont().deriveFont(Font.PLAIN)); } } // if it's a default value, print it gray if ((row > 0) && (value != null) && (value instanceof String)) { if (value .equals(inspecting.getActiveTask().getParameters()[row - 1] .getDefaultValue())) { c.setForeground(Color.GRAY); } else { c.setForeground(Color.BLACK); } } else if ((row > 0) && (c instanceof JTextFieldWithButton)) { JTextFieldWithButton tfwb = (JTextFieldWithButton) c; if (tfwb.getValue().equals( inspecting.getActiveTask().getParameters()[row - 1] .getDefaultValue())) { tfwb.setValueForeground(Color.GRAY); } else { tfwb.setValueForeground(Color.BLACK); } } // suppress "selected" borders // does not work for edited cells (in Metal LnF at least) if (c instanceof JComponent) { JComponent jc = (JComponent) c; jc.setBorder(null); jc.setOpaque(true); if ((column == 1) && (row > 0)) { if (!inspecting.getActiveTask().getParameters()[row - 1] .isValid()) { jc.setBorder(BorderFactory.createLineBorder(Color.RED, 2)); } } } return c; } } /* InspectorPanelTableRenderer */ }