/** * Copyright (C) 2015 Valkyrie RCP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.valkyriercp.form.builder; import org.springframework.context.MessageSource; import org.valkyriercp.binding.form.FormModel; import org.valkyriercp.binding.value.ValueChangeDetector; import org.valkyriercp.binding.value.support.ValueHolder; import org.valkyriercp.factory.AbstractControlFactory; import org.valkyriercp.image.IconSource; import org.valkyriercp.util.OverlayHelper; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Locale; /** * Adds a "dirty overlay" to a component that is triggered by user editing. The * overlaid image is retrieved by the image key "dirty.overlay". The image is * placed at the top-left corner of the component, and the image's tooltip is * set to a message (retrieved with key "dirty.message") such as "{field} has * changed, original value was {value}.". It also adds a small revert button * that resets the value of the field. * * @author Peter De Bruycker */ public class DirtyIndicatorInterceptor extends AbstractFormComponentInterceptor { private static final String DIRTY_ICON_KEY = "dirty.overlay"; private static final String DIRTY_MESSAGE_KEY = "dirty.message"; private static final String REVERT_ICON_KEY = "revert.overlay"; private static final String REVERT_MESSAGE_KEY = "revert.message"; public DirtyIndicatorInterceptor(FormModel formModel) { super(formModel); } public void processComponent(final String propertyName, final JComponent component) { final OriginalValueHolder originalValueHolder = new OriginalValueHolder(); final DirtyOverlay overlay = new DirtyOverlay(getFormModel(), propertyName, originalValueHolder); final ValueHolder reset = new ValueHolder(Boolean.FALSE); getFormModel().getValueModel(propertyName).addValueChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (reset.getValue() == Boolean.TRUE) { originalValueHolder.reset(); reset.setValue(Boolean.FALSE); overlay.setVisible(false); return; } if (!originalValueHolder.isInitialized()) { originalValueHolder.setOriginalValue(evt.getOldValue()); } Object oldValue = originalValueHolder.getValue(); Object newValue = evt.getNewValue(); overlay.setVisible(getValueChangeDetector().hasValueChanged(oldValue, newValue) && !getFormModel().getFieldMetadata(propertyName).isReadOnly()); } }); getFormModel().getFormObjectHolder().addValueChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { // reset original value, new "original" value is in the form // model as the form object has changed reset.setValue(Boolean.TRUE); } }); InterceptorOverlayHelper.attachOverlay(overlay.getControl(), component, OverlayHelper.NORTH_WEST, 5, 0); overlay.setVisible(false); } private ValueChangeDetector getValueChangeDetector() { return getApplicationConfig().valueChangeDetector(); } private static class DirtyOverlay extends AbstractControlFactory { private JButton revertButton; private JLabel dirtyLabel; private FormModel formModel; private String propertyName; private OriginalValueHolder originalValueHolder; public DirtyOverlay(FormModel formModel, String propertyName, OriginalValueHolder originalValueHolder) { this.formModel = formModel; this.propertyName = propertyName; this.originalValueHolder = originalValueHolder; } protected JComponent createControl() { final JPanel control = new JPanel(new BorderLayout()) { public void repaint() { // hack for RCP-426: if the form component is on a tabbed // pane, when switching between tabs when the overlay is // visible, the overlay is not correctly repainted. When we // trigger a revalidate here, everything is ok. revalidate(); super.repaint(); } }; control.setName("dirtyOverlay"); control.setOpaque(false); IconSource iconSource = getApplicationConfig().iconSource(); Icon icon = iconSource.getIcon(DIRTY_ICON_KEY); dirtyLabel = new JLabel(icon); control.add(dirtyLabel, BorderLayout.CENTER); createRevertButton(); control.add(revertButton, BorderLayout.LINE_END); return control; } private void createRevertButton() { IconSource iconSource = getApplicationConfig().iconSource(); Icon icon = iconSource.getIcon(REVERT_ICON_KEY); revertButton = new JButton(icon); revertButton.setBorderPainted(false); revertButton.setContentAreaFilled(false); revertButton.setFocusable(false); revertButton.setMargin(new Insets(-3, -3, -3, -3)); revertButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { // reset formModel.getValueModel(propertyName).setValue(originalValueHolder.getValue()); } }); } public void setVisible(boolean visible) { getControl().setVisible(visible); // manually set the size, otherwise sometimes the overlay is not // shown (it has size 0,0) getControl().setSize(getControl().getPreferredSize()); if (visible) { MessageSource messageSource = getApplicationConfig().messageSource(); String dirtyTooltip = messageSource.getMessage(DIRTY_MESSAGE_KEY, new Object[] { formModel.getFieldFace(propertyName).getDisplayName(), originalValueHolder.getValue() }, Locale .getDefault()); dirtyLabel.setToolTipText(dirtyTooltip); String revertTooltip = messageSource.getMessage(REVERT_MESSAGE_KEY, new Object[] { formModel .getFieldFace(propertyName).getDisplayName() }, Locale.getDefault()); revertButton.setToolTipText(revertTooltip); } } } private static class OriginalValueHolder { private boolean initialized; private Object originalValue; public void setOriginalValue(Object value) { initialized = true; originalValue = value; } public void reset() { initialized = false; originalValue = null; } public Object getValue() { return originalValue; } public boolean isInitialized() { return initialized; } } }