package gui; import com.jgoodies.forms.factories.CC; import com.jgoodies.forms.layout.FormLayout; import com.jidesoft.pane.CollapsiblePane; import gui.events.DataChangeEvent; import gui.events.RelaxedDocumentListener; import gui.interfaces.*; import op.OPDE; import op.threads.DisplayMessage; import op.tools.SYSConst; import op.tools.SYSTools; import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.beanutils.Converter; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.collections.Closure; import org.apache.commons.lang3.ArrayUtils; import org.apache.log4j.Logger; import org.jdesktop.swingx.HorizontalLayout; import javax.swing.*; import javax.swing.colorchooser.ColorSelectionModel; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.constraints.Size; import java.awt.*; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.beans.PropertyVetoException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.sql.SQLIntegrityConstraintViolationException; import java.text.ParseException; import java.util.HashSet; import java.util.Properties; /** * * This Panel accepts annotated Beans which describe a form that needs to be entered. * It uses the annotations from the class gui.interfaces.EditorComponent to tell this editor * how to handle the values of the Bean Class. Only fields which are annotated as EditorComponent * are handled here. The others are ignored. * * there is a data object in the parent class which is automatically initialized by the constructor * * * So why all this fuss ? This class makes the creation of an editor very easy. We simply define a Bean class * with all the constraints we want to be obeyed during the edit phase. And this class handles it in no time. * All the constraints are taken care of by the javax.validation framework. * */ public class PnlBeanEditor<T> extends EditPanelDefault<T> { private final Class<T> clazz; private Closure cancelCallback; // private JPanel customPanel; // private final String[][] fields; private boolean ignoreListener = false; private Logger logger = Logger.getLogger(this.getClass()); private HashSet<Component> componentSet; public static final int SAVE_MODE_IMMEDIATE = 0; public static final int SAVE_MODE_OK_CANCEL = 1; public static final int SAVE_MODE_CUSTOM = 2; // requires ButtonPanel private int saveMode; /** * constructor * * @param dataProvider is the implementation of the DataProvider which is used every time this editor needs the data to display * @param clazz is the class object of the bound class for this editor. technical reasons. no big deal. * @param saveMode tells the editor when to save its contents to the ancestor's data object * @throws IllegalAccessException * @throws NoSuchMethodException * @throws InvocationTargetException */ public PnlBeanEditor(DataProvider<T> dataProvider, Class<T> clazz, int saveMode) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { super(dataProvider); cancelCallback = null; this.clazz = clazz; setOpaque(false); this.saveMode = saveMode; this.componentSet = new HashSet<>(); initPanel(); initButtonPanel(); } public PnlBeanEditor(DataProvider<T> dataProvider, Class<T> clazz) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { this(dataProvider, clazz, SAVE_MODE_CUSTOM); } // public PnlBeanEditor(DataProvider<T> dataProvider, Class<T> clazz, Closure cancelCallback) // throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { // this(dataProvider, clazz, SAVE_MODE_OK_CANCEL); // this.cancelCallback = cancelCallback; // } public void setCustomPanel(JPanel customPanel) { add(customPanel, CC.xyw(1, componentSet.size() * 2 + 2, 5, CC.FILL, CC.FILL)); } @Override public void setBackground(Color bg) { super.setBackground(bg); if (componentSet == null) return; for (Component comp : componentSet) { comp.setBackground(GUITools.blend(bg, Color.WHITE, 0.2f)); } } @Override public void setOpaque(boolean isOpaque) { super.setOpaque(isOpaque); if (componentSet == null) return; for (Component comp : componentSet) { if (comp instanceof JColorChooser) { ((JColorChooser) comp).setOpaque(isOpaque); } } } /** * creates the panel programmically according to the definitions in the bean blass T. * * @throws IllegalAccessException * @throws NoSuchMethodException * @throws InvocationTargetException */ void initPanel() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { // fields means the fields of the BeanClass which defines the bahavior of the editor Field[] fields = data.getClass().getDeclaredFields(); // I have to count them first. // makes sure, that we only care about fields which are meant for this editor. int numfields = 0; for (final Field field : fields) { if (field.isAnnotationPresent(EditorComponent.class)) { numfields++; } } // create a simple formlayout which is extendable setLayout(new FormLayout("5dlu, default, $lcgap, 162dlu:grow, $lcgap, 5dlu", "5dlu, " + (numfields + (saveMode == SAVE_MODE_IMMEDIATE ? 0 : 1) + "*(default, $lgap), default, 5dlu"))); int row = 1; // deal with every single field for (final Field field : fields) { if (field.isAnnotationPresent(EditorComponent.class)) { // we only care about EditorComponents EditorComponent editorComponent = field.getAnnotation(EditorComponent.class); JLabel lblName = new JLabel(SYSTools.xx(editorComponent.label())); lblName.setFont(new Font("Arial", Font.BOLD, 14)); JComponent comp = null; // this section handles every single type of editor component if (editorComponent.component()[0].equalsIgnoreCase("textfield")) { JPanel innerPanel = new JPanel(); innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.LINE_AXIS)); final JLabel lblOK = new JLabel(SYSConst.icon16apply); final JTextField txt; if (field.isAnnotationPresent(Size.class)) { Size sizeConstraint = field.getAnnotation(Size.class); txt = new BoundedTextField(sizeConstraint.min(), sizeConstraint.max()); } else { txt = new JTextField(); } // the data object sets the contents of the textfield txt.setText(PropertyUtils.getProperty(data, field.getName()).toString()); txt.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { ((JTextField) e.getSource()).selectAll(); } }); // changes to the textfield are handled by a document listener txt.getDocument().addDocumentListener(new RelaxedDocumentListener(de -> { if (ignoreListener) return; if (saveMode == SAVE_MODE_IMMEDIATE) reload(); try { String text = de.getDocument().getText(0, de.getDocument().getLength()); Object value = text; // if there is a parser defined. it is loaded here if (!editorComponent.parserClass().isEmpty()) { String p = editorComponent.parserClass(); logger.debug(String.format("Parserclass for Field %s: %s", field.getName(), p)); // loads the specified parser class (needs to have a "parse" method) Class parserClazz = Class.forName(p); value = parserClazz.getMethod("parse", String.class).invoke(parserClazz.newInstance(), text); logger.debug(String.format("Parsed value: %s", value.toString())); } PropertyUtils.setProperty(data, field.getName(), ConvertUtils.convert(value, field.getType())); // ConverterUtils fixes #25 // logger.debug(String.format("Content of the 'data' object: %s", ((Properties) data).getProperty(field.getName()))); if (saveMode == SAVE_MODE_IMMEDIATE) { broadcast(); // spread the news, that the data object was updated. in case of a contraint violation, an exception is thrown } OPDE.getDisplayManager().clearSubMessages(); lblOK.setIcon(SYSConst.icon16apply); lblOK.setToolTipText(null); GUITools.flashBackground(txt, SYSConst.darkolivegreen1, Color.white, 2); } catch (BadLocationException e1) { OPDE.error(logger, e1); lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_bold(e1.getMessage()))); } catch (IllegalAccessException e1) { OPDE.error(logger, e1); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_bold(e1.getMessage()))); } catch (InvocationTargetException ite) { if (ite.getTargetException() instanceof ParseException) { OPDE.getDisplayManager().addSubMessage(new DisplayMessage(ite.getTargetException().getMessage(), DisplayMessage.WARNING)); } else { OPDE.error(logger, ite); } lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_bold(ite.getMessage()))); } catch (NoSuchMethodException e) { OPDE.error(logger, e); lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_bold(e.getMessage()))); } catch (ConstraintViolationException cve) { logger.debug("Constraint violation!!"); if (saveMode == SAVE_MODE_IMMEDIATE) { OPDE.getDisplayManager().addSubMessage(new DisplayMessage(cve)); } String message = ""; for (ConstraintViolation cv : cve.getConstraintViolations()) { logger.debug(cv.getPropertyPath().toString()); logger.debug(field.getName()); if (cv.getPropertyPath().toString().equals(field.getName())) { message = cv.getMessage(); break; } } if (message.isEmpty()) { OPDE.getDisplayManager().clearSubMessages(); lblOK.setIcon(SYSConst.icon16apply); lblOK.setToolTipText(null); GUITools.flashBackground(txt, SYSConst.darkolivegreen1, Color.white, 2); } else { lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_paragraph(message))); GUITools.flashBackground(txt, SYSConst.orangered, Color.white, 2); } } catch (ClassNotFoundException e) { OPDE.error(logger, e); lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_bold(e.getMessage()))); } catch (InstantiationException e) { OPDE.error(logger, e); lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen(SYSConst.html_bold(e.getMessage()))); } catch (SQLIntegrityConstraintViolationException e) { OPDE.warn(logger, e); lblOK.setIcon(SYSConst.icon16cancel); lblOK.setToolTipText(SYSTools.toHTMLForScreen("error.sql.integrity")); GUITools.flashBackground(txt, SYSConst.orangered, Color.white, 2); } })); innerPanel.add(txt); innerPanel.add(lblOK); innerPanel.setOpaque(false); comp = innerPanel; comp.setName("innerPanel"); txt.setName(field.getName()); } else if (editorComponent.component()[0].equalsIgnoreCase("combobox")) { // this is the default ItemListener, if there is no renderer defined ItemListener il = e -> { if (ignoreListener) return; if (e.getStateChange() == ItemEvent.SELECTED) { if (saveMode == SAVE_MODE_IMMEDIATE) reload(); try { PropertyUtils.setProperty(data, field.getName(), field.getType().cast(((JComboBox) e.getSource()).getSelectedIndex())); if (saveMode == SAVE_MODE_IMMEDIATE) broadcast(); } catch (Exception e1) { logger.debug(e1); } } }; ListCellRenderer<T> renderer = null; DefaultComboBoxModel dcbm = new DefaultComboBoxModel<>(ArrayUtils.subarray(editorComponent.component(), 1, editorComponent.component().length - 1)); try { if (!editorComponent.renderer().isEmpty()) { String r = editorComponent.renderer(); Class rendererClazz = Class.forName(r); renderer = (ListCellRenderer) rendererClazz.newInstance(); String m = editorComponent.model(); Class modelClazz = Class.forName(m); dcbm = (DefaultComboBoxModel) modelClazz.newInstance(); // if there is a renderer weg got for the object itself, rather than the selected index il = e -> { if (e.getStateChange() == ItemEvent.SELECTED) { if (ignoreListener) return; if (saveMode == SAVE_MODE_IMMEDIATE) reload(); try { PropertyUtils.setProperty(data, field.getName(), field.getType().cast(((JComboBox) e.getSource()).getSelectedItem())); if (saveMode == SAVE_MODE_IMMEDIATE) broadcast(); } catch (Exception e1) { logger.debug(e1); } } }; } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } JComboBox combobox = new JComboBox(dcbm); if (renderer != null) { combobox.setSelectedItem(PropertyUtils.getProperty(data, field.getName())); } else { // https://github.com/tloehr/Offene-Pflege.de/issues/29 combobox.setSelectedIndex(Integer.parseInt(PropertyUtils.getProperty(data, field.getName()).toString())); } combobox.addItemListener(il); if (renderer != null) combobox.setRenderer(renderer); comp = combobox; comp.setName(field.getName()); } else if (editorComponent.component()[0].equalsIgnoreCase("onoffswitch")) { String yesText = "misc.msg.yes"; String noText = "misc.msg.no"; if (editorComponent.component().length == 3) { yesText = editorComponent.component()[1]; noText = editorComponent.component()[2]; } comp = new YesNoToggleButton(yesText, noText, (boolean) PropertyUtils.getProperty(data, field.getName())); comp.setName(field.getName()); ((YesNoToggleButton) comp).addItemListener(e -> { if (ignoreListener) return; if (saveMode == SAVE_MODE_IMMEDIATE) reload(); try { PropertyUtils.setProperty(data, field.getName(), new Boolean(e.getStateChange() == ItemEvent.SELECTED)); if (saveMode == SAVE_MODE_IMMEDIATE) broadcast(); } catch (Exception e1) { logger.debug(e1); } }); } else if (editorComponent.component()[0].equalsIgnoreCase("colorset")) { JColorChooser clr = new JColorChooser(GUITools.getColor(PropertyUtils.getProperty(data, field.getName()).toString())); clr.getSelectionModel().addChangeListener(e -> { if (saveMode == SAVE_MODE_IMMEDIATE) reload(); try { PropertyUtils.setProperty(data, field.getName(), GUITools.getHTMLColor(((ColorSelectionModel) e.getSource()).getSelectedColor())); DataChangeEvent<T> dce = new DataChangeEvent(thisPanel, data); dce.setTriggersReload(true); if (saveMode == SAVE_MODE_IMMEDIATE) broadcast(); } catch (Exception e1) { logger.debug(e1); } }); CollapsiblePane cp = new CollapsiblePane(SYSTools.xx(editorComponent.label())); cp.setStyle(CollapsiblePane.PLAIN_STYLE); cp.setIcon(SYSConst.icon22colorset); try { cp.setCollapsed(true); } catch (PropertyVetoException e) { //bah } cp.setOpaque(false); cp.setContentPane(clr); comp = cp; comp.setName(field.getName()); } else { OPDE.fatal(logger, new IllegalStateException("invalid component name in EditorComponent Annotation")); } if (comp instanceof CollapsiblePane) { add(comp, CC.xyw(2, row + 1, 4, CC.FILL, CC.DEFAULT)); } else if (editorComponent.filled().equals("false")) { add(lblName, CC.xy(2, row + 1, CC.LEFT, CC.TOP)); add(comp, CC.xy(4, row + 1, CC.LEFT, CC.TOP)); } else { add(lblName, CC.xy(2, row + 1, CC.LEFT, CC.TOP)); add(comp, CC.xy(4, row + 1)); } SYSTools.setXEnabled(comp, editorComponent.readonly().equals("false")); // comp.setEnabled(); comp.setToolTipText(editorComponent.tooltip().isEmpty() ? null : SYSTools.xx(editorComponent.tooltip())); componentSet.add(comp); row += 2; } } } void initButtonPanel() { if (saveMode != SAVE_MODE_OK_CANCEL) return; JPanel buttonPanel = new JPanel(new HorizontalLayout(5)); JButton btnOK = new JButton(SYSConst.icon22apply); btnOK.addActionListener(e -> { try { broadcast(new DataChangeEvent(thisPanel, data)); } catch (ConstraintViolationException cve) { OPDE.getDisplayManager().addSubMessage(new DisplayMessage(cve)); } catch (InvocationTargetException e1) { e1.printStackTrace(); } catch (NoSuchMethodException e1) { e1.printStackTrace(); } catch (IllegalAccessException e1) { e1.printStackTrace(); } catch (SQLIntegrityConstraintViolationException e1) { OPDE.warn(logger, e1); OPDE.getDisplayManager().addSubMessage(new DisplayMessage(e1.getMessage())); } if (saveMode == SAVE_MODE_IMMEDIATE) reload(); }); buttonPanel.add(btnOK); JButton btnCancel = new JButton(SYSConst.icon22cancel); btnCancel.addActionListener(e -> { if (cancelCallback == null) { reload(); refreshDisplay(); } else { cancelCallback.execute(null); } // revert to old bean state }); buttonPanel.add(btnCancel); add(buttonPanel, CC.xyw(1, componentSet.size() * 2 + 2, 5, CC.RIGHT, CC.DEFAULT)); } // @Override // public void broadcast() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { // if (saveMode != SAVE_MODE_IMMEDIATE) return; // super.broadcast(new DataChangeEvent(thisPanel, data)); // } /** * causes the editor to send the current state of its data to all listeners. * * @throws IllegalAccessException * @throws NoSuchMethodException * @throws InvocationTargetException */ public void broadcast() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, SQLIntegrityConstraintViolationException { super.broadcast(new DataChangeEvent(thisPanel, data)); } // @Override // public void broadcast(DataChangeEvent<T> dce) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { // if (saveMode != SAVE_MODE_IMMEDIATE) return; // super.broadcast(new DataChangeEvent(thisPanel, data)); // } @Override public void setStartFocus() { } @Override public void setEnabled(boolean enabled) { for (Component comp : componentSet) { comp.setEnabled(enabled); } super.setEnabled(enabled); } @Override public String doValidation() { return null; } @Override public void refreshDisplay() { ignoreListener = true; for (Component comp : componentSet) { if (comp instanceof JPanel && comp.getName().equals("innerPanel")) { // textcomponents are embedded in a JPanel for (Component innerComp : ((JPanel) comp).getComponents()) { if (innerComp instanceof JTextComponent) { try { ((JTextComponent) innerComp).setText(PropertyUtils.getProperty(data, innerComp.getName()).toString()); } catch (IllegalAccessException e) { OPDE.error(logger, e); } catch (InvocationTargetException e) { OPDE.error(logger, e); } catch (NoSuchMethodException e) { OPDE.error(logger, e); } } } } else if (comp instanceof YesNoToggleButton) { try { ((YesNoToggleButton) comp).setSelected((boolean) PropertyUtils.getProperty(data, comp.getName())); } catch (IllegalAccessException e) { OPDE.error(logger, e); } catch (InvocationTargetException e) { OPDE.error(logger, e); } catch (NoSuchMethodException e) { OPDE.error(logger, e); } } else if (comp instanceof JComboBox) { //TODO: Das muss noch fertig werden JComboBox combobox = (JComboBox) comp; // if (renderer != null) { // combobox.setSelectedItem(PropertyUtils.getProperty(data, field.getName())); // } else { // combobox = new JComboBox(new DefaultComboBoxModel<>(ArrayUtils.subarray(editorComponent.component(), 1, editorComponent.component().length - 1))); // } } } ignoreListener = false; } }