/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.server.headlessclient.dataui; import java.awt.Dimension; import java.awt.Insets; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.swing.SwingConstants; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import org.apache.wicket.Component; import org.apache.wicket.model.IModel; import com.servoy.j2db.dataprocessing.IDisplayData; import com.servoy.j2db.persistence.ISupportTextSetup; import com.servoy.j2db.ui.IStylePropertyChanges; import com.servoy.j2db.ui.IStylePropertyChangesRecorder; import com.servoy.j2db.util.ComponentFactoryHelper; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.IStyleSheet; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.PersistHelper; import com.servoy.j2db.util.ServoyStyleSheet; import com.servoy.j2db.util.Utils; import com.servoy.j2db.util.gui.ISupportCustomBorderInsets; /** * This class records the changes for wicket components/beans in ajax mode. * It has a {@link #setChanged()} method for marking the component for render and helper methods for generating the right css properties like location and font * <p> * when calling {@link #setChanged()} on it the component will be re rendered the next time a (ajax) request comes in * This can be the ajax polling behavior that every page of a servoy application has if ajax mode is enabled. * </p> * When setChanged() is called or any other helper method you have to call {@link #setRendered()} when the component is rendered again * else it will be re rendered for every coming request. This can be done by calling {@link #setRendered()} from the {@link Component#onAfterRender()} * that the wicket component needs to override. * <p> * the helper methods should be called from javascript methods so that changes done by javascript are reflected in the browser. * </p> * @author jcompagner * @since 5.0 */ public class ChangesRecorder implements IStylePropertyChangesRecorder { private static final ConcurrentMap<Integer, String> SIZE_STRINGS = new ConcurrentHashMap<Integer, String>(); private final Properties changedProperties = new Properties(); private final Properties jsProperties = new Properties(); private String bgcolor; private Insets defaultBorder; private Insets defaultPadding; private boolean changed; private boolean valueChanged; private IStylePropertyChanges additionalChangesRecorder; /** * default constructor if the component doesnt have default border or padding. */ public ChangesRecorder() { this(null, null); } /** * use this constructor if the component for which this change recorder is made has a default border or padding in the browser. * So that size calculations will take that into account. * * @param defaultBorder * @param defaultPadding */ public ChangesRecorder(Insets defaultBorder, Insets defaultPadding) { this.defaultBorder = defaultBorder; this.defaultPadding = defaultPadding; } public void setDefaultBorderAndPadding(Insets defaultBorder, Insets defaultPadding) { this.defaultBorder = defaultBorder; this.defaultPadding = defaultPadding; } /** Additional changes recorder for inner components that may change requiring the entire component to be rendered * * @param additionalChangesRecorder the additionalChangesRecorder to set */ public void setAdditionalChangesRecorder(IStylePropertyChanges additionalChangesRecorder) { this.additionalChangesRecorder = additionalChangesRecorder; } /** * @return All the current css properties of this component */ public Properties getChanges() { return changedProperties; } /** * Adds all the css properties to the changed set and calls setChanged() * * @param changes */ public void setChanges(Properties changes) { if ("none".equals(changedProperties.getProperty("display")) && !changes.containsKey("display")) { changedProperties.remove("display"); } removePropertyIfNotPresent(changedProperties, changes, "color"); removePropertyIfNotPresent(changedProperties, changes, "background-color"); removePropertyIfNotPresent(changedProperties, changes, "font-family"); removePropertyIfNotPresent(changedProperties, changes, "font-size"); removePropertyIfNotPresent(changedProperties, changes, "font-style"); removePropertyIfNotPresent(changedProperties, changes, "font-weight"); changedProperties.putAll(changes); if (!IStyleSheet.COLOR_TRANSPARENT.equals(changes.getProperty("background-color"))) //$NON-NLS-1$ { bgcolor = changes.getProperty("background-color"); //$NON-NLS-1$ } setChanged(); } private void removePropertyIfNotPresent(Properties oldProperties, Properties newProperties, String property) { if (oldProperties.containsKey(property) && !newProperties.containsKey(property)) { changedProperties.remove(property); } } /** * Adds the background-color css property for the given color to the changed properties set. * * @param bgcolor */ public void setBgcolor(String bgcolor) { if (!Utils.equalObjects(this.bgcolor, bgcolor)) { this.bgcolor = bgcolor; if (!IStyleSheet.COLOR_TRANSPARENT.equals(changedProperties.getProperty("background-color"))) //$NON-NLS-1$ { setChanged(); if (bgcolor == null) { changedProperties.remove("background-color"); //$NON-NLS-1$ } else { changedProperties.put("background-color", bgcolor); //$NON-NLS-1$ } } } } /** * Adds the color css property for the given color to the changed properties set. * * @param clr */ public void setFgcolor(String clr) { if (!Utils.equalObjects(changedProperties.get("color"), clr)) //$NON-NLS-1$ { setChanged(); if (clr == null) { changedProperties.remove("color"); //$NON-NLS-1$ } else { changedProperties.put("color", clr); //$NON-NLS-1$ } } } /** * Adds the border css property for the given color to the changed properties set. * * @param border */ public void setBorder(String border) { setChanged(); if (border != null) { Properties properties = new Properties(); if (!border.contains(ComponentFactoryHelper.ROUNDED_BORDER)) { for (String prefix : ServoyStyleSheet.ROUNDED_RADIUS_PREFIX) { properties.put(prefix + "border-radius", "0px"); } properties.put("border-radius", "0px"); } ComponentFactoryHelper.createBorderCSSProperties(border, properties); changedProperties.putAll(properties); } else { changedProperties.put("border-style", "none"); //$NON-NLS-1$ //$NON-NLS-2$ changedProperties.remove("border-width"); //$NON-NLS-1$ changedProperties.remove("border-color"); //$NON-NLS-1$ changedProperties.remove("border-radius"); //$NON-NLS-1$ for (String prefix : ServoyStyleSheet.ROUNDED_RADIUS_PREFIX) { changedProperties.remove(prefix + "border-radius"); } } } /** * Sets the background-color css property to transparent if the boolean is true, * if false then it test if it has to set the bgcolor or remove the background-color property * * @param transparent */ public void setTransparent(boolean transparent) { if (transparent) { if (!IStyleSheet.COLOR_TRANSPARENT.equals(changedProperties.getProperty("background-color"))) { changedProperties.put("background-color", IStyleSheet.COLOR_TRANSPARENT); //$NON-NLS-1$ setChanged(); } } else if (bgcolor != null) { if (!Utils.equalObjects(changedProperties.get("background-color"), bgcolor)) { changedProperties.put("background-color", bgcolor); //$NON-NLS-1$ setChanged(); } } else { if (changedProperties.containsKey("background-color")) { changedProperties.remove("background-color"); setChanged(); } } } /** * Sets the x,y location css properties to the changed set. * * @param x * @param y */ public void setLocation(int x, int y) { setChanged(); changedProperties.put("left", getSizeString(x)); //$NON-NLS-1$ changedProperties.put("top", getSizeString(y)); //$NON-NLS-1$ } /** * @param width * @param height */ public void setSize(int width, int height, Border border, Insets margin, int fontSize) { setSize(width, height, border, margin, fontSize, false, SwingConstants.CENTER); } private Dimension oldSize = null; public void setSize(int width, int height, Border border, Insets margin, int fontSize, boolean isButtonOrSelect, int valign) { Dimension realSize = calculateWebSize(width, height, border, margin, fontSize, changedProperties, isButtonOrSelect, valign); if (!Utils.equalObjects(realSize, oldSize)) { setChanged(); oldSize = realSize; } } public Dimension calculateWebSize(int width, int height, Border border, Insets margin, int fontSize, Properties properties) { return calculateWebSize(width, height, border, margin, fontSize, properties, false, SwingConstants.CENTER); } public String getJSProperty(String key) { return jsProperties.getProperty(key); } public Dimension calculateWebSize(int width, int height, Border border, Insets margin, int fontSize, Properties properties, boolean isButtonOrSelect, int valign) { jsProperties.put("offsetWidth", getSizeString(width)); //$NON-NLS-1$ jsProperties.put("offsetHeight", getSizeString(height)); //$NON-NLS-1$ Insets insets = getPaddingAndBorder(height, border, margin, fontSize, properties, isButtonOrSelect, valign); int realWidth = width; int realheight = height; // for <button> and <select> tags the border is drawn inside the component, regardless of the box model if (insets != null && !isButtonOrSelect) { realWidth -= (insets.left + insets.right); realheight -= (insets.top + insets.bottom); } if (realWidth < 0) realWidth = 0; if (realheight < 0) realheight = 0; if (properties != null) { properties.put("width", getSizeString(realWidth)); //$NON-NLS-1$ properties.put("height", getSizeString(realheight)); //$NON-NLS-1$ } return new Dimension(realWidth, realheight); } public Insets getPaddingAndBorder(int height, Border border, Insets margin, int fontSize, Properties properties) { return getPaddingAndBorder(height, border, margin, fontSize, properties, false, SwingConstants.CENTER); } /** * @param height * @param border * @param margin * @param fontSize * @param properties * @return the padding and border */ @SuppressWarnings("nls") public Insets getPaddingAndBorder(int height, Border border, Insets margin, int fontSize, Properties properties, boolean isButtonOrSelect, int valign) { Insets insets = null; Insets borderMargin = margin; if (border != null) { // labels do have compound borders where margin and border are stored in. if (border instanceof CompoundBorder) { Insets marginInside = ComponentFactoryHelper.getBorderInsetsForNoComponent(((CompoundBorder)border).getInsideBorder()); borderMargin = TemplateGenerator.sumInsets(borderMargin, marginInside); Border ob = ((CompoundBorder)border).getOutsideBorder(); if (ob instanceof ISupportCustomBorderInsets) { insets = ((ISupportCustomBorderInsets)ob).getCustomBorderInsets(); } else { insets = ComponentFactoryHelper.getBorderInsetsForNoComponent(ob); } } else if (border instanceof ISupportCustomBorderInsets) { insets = ((ISupportCustomBorderInsets)border).getCustomBorderInsets(); } else { try { insets = ComponentFactoryHelper.getBorderInsetsForNoComponent(border); } catch (Exception ex) { insets = defaultBorder; Debug.error(ex); } } } else { insets = defaultBorder; } Insets padding = borderMargin; if (padding == null) padding = defaultPadding; if (properties != null) { Insets borderAndPadding = TemplateGenerator.sumInsets(insets, padding); int innerHeight = height; if (borderAndPadding != null) innerHeight -= borderAndPadding.top + borderAndPadding.bottom; int bottomPaddingExtra = 0; if (isButtonOrSelect && valign != ISupportTextSetup.CENTER) { bottomPaddingExtra = innerHeight; } if (padding == null) { properties.put("padding-top", "0px"); properties.put("padding-right", "0px"); properties.put("padding-left", "0px"); properties.put("padding-bottom", getSizeString(bottomPaddingExtra)); } else { properties.put("padding-top", getSizeString(padding.top)); properties.put("padding-right", getSizeString(padding.right)); properties.put("padding-left", getSizeString(padding.left)); properties.put("padding-bottom", getSizeString((bottomPaddingExtra + padding.bottom))); } } if (insets == null) insets = padding; else { insets = TemplateGenerator.sumInsets(insets, padding); } return insets; } public Insets getPadding(Border border, Insets margin) { Insets borderMargin = margin; if (border != null) { if (border instanceof CompoundBorder) { Insets marginInside = ComponentFactoryHelper.getBorderInsetsForNoComponent(((CompoundBorder)border).getInsideBorder()); borderMargin = TemplateGenerator.sumInsets(borderMargin, marginInside); } } return (borderMargin == null ? defaultPadding : borderMargin); } /** * @param spec */ public void setFont(String spec) { setChanged(); Pair<String, String>[] props = PersistHelper.createFontCSSProperties(spec); if (props != null) { for (Pair<String, String> element : props) { if (element == null) continue; changedProperties.put(element.getLeft(), element.getRight()); } } else { changedProperties.remove("font-family"); //$NON-NLS-1$ changedProperties.remove("font-size"); //$NON-NLS-1$ changedProperties.remove("font-style"); //$NON-NLS-1$ changedProperties.remove("font-weight"); //$NON-NLS-1$ } } /** * @param visible */ public void setVisible(boolean visible) { setChanged(); if (visible) { changedProperties.remove("display"); //$NON-NLS-1$ } else { changedProperties.put("display", "none"); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Call this method from the {@link Component#onBeforeRender()} call to let the change recorder know it has been rendered. */ public void setRendered() { changed = false; valueChanged = false; if (additionalChangesRecorder != null) { additionalChangesRecorder.setRendered(); } } /** * Returns true if this change recorder is changed and its component will be rendered the next time. * * @see com.servoy.j2db.ui.IStylePropertyChanges#isChanged() */ public boolean isChanged() { return changed || (additionalChangesRecorder != null && additionalChangesRecorder.isChanged()); } /** * returns true if its component model object is changed * * @see com.servoy.j2db.ui.IStylePropertyChanges#isValueChanged() */ public boolean isValueChanged() { return valueChanged; } /** * Set the change flag to true so that the component will be rendered the next time. * */ public void setChanged() { changed = true; } /** * sets the value changed to true so that servoy knows that it is the value object that is changed. * * @see com.servoy.j2db.ui.IStylePropertyChanges#setValueChanged() */ public void setValueChanged() { valueChanged = true; changed = true; } /** * Helper method to see if the value is changed. * * @param component * @param value */ public void testChanged(Component component, Object value) { IModel model = component.getInnermostModel(); if (model instanceof RecordItemModel) { Object o = ((RecordItemModel)model).getLastRenderedValue(component); Object displayV = value; if (component instanceof IResolveObject) { displayV = ((IResolveObject)component).resolveDisplayValue(value); } if (component instanceof IDisplayData && ((IDisplayData)component).getDataProviderID() == null) { // we don't have a mechanism to detect if the text has changed // both oldvalue and newvalue will always be null changed = true; } else if (!Utils.equalObjects(o, displayV)) { changed = true; valueChanged = true; } } } private static String getSizeString(int size) { Integer integer = Integer.valueOf(size); String string = SIZE_STRINGS.get(integer); if (string == null) { string = size + "px"; //$NON-NLS-1$ SIZE_STRINGS.put(integer, string); } return string; } }