/****************************************************************************** * Product: Posterita Ajax UI * * Copyright (C) 2007 Posterita Ltd. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along * * with this program; if not, write to the Free Software Foundation, Inc., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * * For the text or an alternative of this public license, you may reach us * * Posterita Ltd., 3, Draper Avenue, Quatre Bornes, Mauritius * * or via info@posterita.org or http://www.posterita.org/ * *****************************************************************************/ package org.adempiere.webui.component; import java.math.BigDecimal; import java.text.NumberFormat; import java.text.ParseException; import org.adempiere.webui.ClientInfo; import org.adempiere.webui.LayoutUtils; import org.adempiere.webui.theme.ThemeManager; import org.adempiere.webui.util.ZKUpdateUtil; import org.compiere.model.MSysConfig; import org.compiere.util.DisplayType; import org.compiere.util.Env; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.util.Clients; import org.zkoss.zul.Decimalbox; import org.zkoss.zul.Div; import org.zkoss.zul.Hbox; import org.zkoss.zul.Popup; import org.zkoss.zul.Vbox; /** * Composite component of {@link Decimalbox} and {@link Button} * @author Ashley G Ramdass * @date Mar 11, 2007 * @version $Revision: 0.10 $ * * @author Low Heng Sin */ public class NumberBox extends Div { /** * generated serial id */ private static final long serialVersionUID = 8543853599051754172L; /** Text box for calculator */ private Textbox txtCalc = new Textbox(); /** true for integer, false for number with decimal point */ protected boolean integral = false; protected NumberFormat format = null; private Decimalbox decimalBox = null; private Button btn; /** calculator popup */ private Popup popup; /** * @param integral */ public NumberBox(boolean integral) { this(integral, false); } /** * @param integral * @param tableEditor */ public NumberBox(boolean integral, boolean tableEditor) { super(); this.integral = integral; init(tableEditor); } /** * Layout component * @param tableEditor */ private void init(boolean tableEditor) { decimalBox = new Decimalbox(); if (integral) decimalBox.setScale(0); decimalBox.setStyle("display: inline-block;text-align:right"); ZKUpdateUtil.setHflex(decimalBox, "0"); decimalBox.setSclass("editor-input"); decimalBox.setId(decimalBox.getUuid()); char separatorChar = DisplayType.getNumberFormat(DisplayType.Number, null).getDecimalFormatSymbols().getDecimalSeparator(); String separator = Character.toString(separatorChar); boolean processDotKeypad = MSysConfig.getBooleanValue(MSysConfig.ZK_DECIMALBOX_PROCESS_DOTKEYPAD, true, Env.getAD_Client_ID(Env.getCtx())); if (processDotKeypad && ! ".".equals(separator)) { /* this code works for the decimalbox - the calculator is managed in calc.js */ StringBuffer funct = new StringBuffer(); funct.append("function(evt)"); funct.append("{"); // ignore dot, comma and decimal separator and process them on key down funct.append(" if (!this._shallIgnore(evt, '0123456789-%'))"); funct.append(" {"); funct.append(" this.$doKeyPress_(evt);"); funct.append(" }"); funct.append("}"); decimalBox.setWidgetOverride("doKeyPress_", funct.toString()); funct = new StringBuffer(); // debug // funct.append("console.log('keyCode='+event.keyCode);"); funct.append("(function(event) {"); funct.append("let key=0;"); funct.append("if (window.event)"); funct.append(" key = event.keyCode;"); funct.append("else"); funct.append(" key = event.which;"); funct.append("if (key == 108 || key == 110 || key == 188 || key == 190 || key == 194) {"); funct.append(" let id = '$'.concat('").append(decimalBox.getId()).append("');"); funct.append(" let calcText = jq(id)[0];"); funct.append(" let position = calcText.selectionStart;"); funct.append(" let newValue = calcText.value.substring(0, position) + '").append(separator).append("' + calcText.value.substring(position);"); funct.append(" calcText.value = newValue;"); funct.append(" calcText.setSelectionRange(position+1, position+1);"); funct.append(" event.stop;"); funct.append("}})(event);"); decimalBox.setWidgetListener("onKeyDown", funct.toString()); } appendChild(decimalBox); btn = new Button(); if (ThemeManager.isUseFontIconForImage()) btn.setIconSclass("z-icon-Calculator"); else btn.setImage(ThemeManager.getThemeResource("images/Calculator16.png")); btn.setTabindex(-1); ZKUpdateUtil.setHflex(btn, "0"); btn.addEventListener(Events.ON_CLICK, new EventListener() { @Override public void onEvent(Event event) throws Exception { if (popup != null) { popup.open(NumberBox.this, "after_start"); // Fill the calculator with the actual value of the field // TODO: this could be made a user preference String curValue = ""; if (decimalBox.getValue() != null) { curValue = decimalBox.getValue().toString(); boolean processDotKeypad = MSysConfig.getBooleanValue(MSysConfig.ZK_DECIMALBOX_PROCESS_DOTKEYPAD, true, Env.getAD_Client_ID(Env.getCtx())); if (processDotKeypad) { char separatorChar = DisplayType.getNumberFormat(DisplayType.Number, null).getDecimalFormatSymbols().getDecimalSeparator(); String separator = Character.toString(separatorChar); curValue = curValue.replace(".", separator); } if ("0".equals(curValue)) { curValue = ""; } } String txtCalcId = txtCalc.getId(); Clients.evalJavaScript("calc.append('" + txtCalcId + "', '" + curValue + "')"); } } }); LayoutUtils.addSclass("editor-button", btn); appendChild(btn); popup = getCalculatorPopup(); appendChild(popup); btn.setStyle("text-align: center;"); LayoutUtils.addSclass("number-box", this); LayoutUtils.addSclass("editor-box", this); if (ClientInfo.isMobile()) { LayoutUtils.addSclass("mobile", decimalBox); btn.setVisible(false); } } /** * Set number format * @param format */ public void setFormat(NumberFormat format) { this.format = format; } /** * Set value to {@link #decimalBox} * @param value */ public void setValue(Object value) { if (value == null) decimalBox.setValue((BigDecimal) null); else if (value instanceof BigDecimal) decimalBox.setValue((BigDecimal) value); else if (value instanceof Number) decimalBox.setValue(BigDecimal.valueOf(((Number)value).doubleValue())); else decimalBox.setValue(new BigDecimal(value.toString())); } /** * Get value from {@link #decimalBox} * @return BigDecimal */ public BigDecimal getValue() { return decimalBox.getValue(); } /** * Get text from {@link #decimalBox} * @return text */ public String getText() { BigDecimal value = decimalBox.getValue(); if (value == null) return null; return decimalBox.getText(); } /** * Set value to {@link #decimalBox} * @param value */ public void setValue(String value) { Number numberValue = null; if (format != null) { try { numberValue = format.parse(value); setValue(numberValue); } catch (ParseException e) { } } else { decimalBox.setValue(new BigDecimal(value)); } } /** * Create calculator popup * @return Popup */ private Popup getCalculatorPopup() { Popup popup = new Popup(); Vbox vbox = new Vbox(); char separatorChar = DisplayType.getNumberFormat(DisplayType.Number, null).getDecimalFormatSymbols().getDecimalSeparator(); String separator = Character.toString(separatorChar); txtCalc = new Textbox(); decimalBox.setId(decimalBox.getUuid()); txtCalc.setId(txtCalc.getUuid()); boolean processDotKeypad = MSysConfig.getBooleanValue(MSysConfig.ZK_DECIMALBOX_PROCESS_DOTKEYPAD, true, Env.getAD_Client_ID(Env.getCtx())); StringBuffer funct = new StringBuffer(); funct.append("function(evt)"); funct.append("{"); if (processDotKeypad) { funct.append(" if (!this._shallIgnore(evt, '= -/()*%+0123456789'))"); } else { // restrict allowed characters String decimalSep = separator; if (!".".equals(separator)) decimalSep += "."; funct.append(" if (!this._shallIgnore(evt, '= -/()*%+0123456789").append(decimalSep).append("'))"); } funct.append(" {"); funct.append(" this.$doKeyPress_(evt);"); funct.append(" }"); funct.append("}"); txtCalc.setWidgetOverride("doKeyPress_", funct.toString()); txtCalc.setWidgetListener("onKeyDown", "calc.validateDown('" + decimalBox.getId() + "','" + txtCalc.getId() + "'," + integral + "," + (int)separatorChar + ", event, " + ( processDotKeypad ? "true" : "false" ) + ");"); txtCalc.setMaxlength(250); txtCalc.setCols(30); String txtCalcId = txtCalc.getId(); vbox.appendChild(txtCalc); Hbox row1 = new Hbox(); Button btnAC = new Button(); ZKUpdateUtil.setWidth(btnAC, "40px"); btnAC.setLabel("AC"); btnAC.setWidgetListener("onClick", "calc.clearAll('" + txtCalcId + "')"); Button btn7 = new Button(); ZKUpdateUtil.setWidth(btn7, "30px"); btn7.setLabel("7"); btn7.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '7')"); Button btn8 = new Button(); ZKUpdateUtil.setWidth(btn8, "30px"); btn8.setLabel("8"); btn8.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '8')"); Button btn9 = new Button(); ZKUpdateUtil.setWidth(btn9, "30px"); btn9.setLabel("9"); btn9.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '9')"); Button btnMultiply = new Button(); ZKUpdateUtil.setWidth(btnMultiply, "30px"); btnMultiply.setLabel("*"); btnMultiply.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', ' * ')"); row1.appendChild(btnAC); row1.appendChild(btn7); row1.appendChild(btn8); row1.appendChild(btn9); row1.appendChild(btnMultiply); Hbox row2 = new Hbox(); Button btnC = new Button(); ZKUpdateUtil.setWidth(btnC, "40px"); btnC.setLabel("C"); btnC.setWidgetListener("onClick", "calc.clear('" + txtCalcId + "')"); Button btn4 = new Button(); ZKUpdateUtil.setWidth(btn4, "30px"); btn4.setLabel("4"); btn4.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '4')"); Button btn5 = new Button(); ZKUpdateUtil.setWidth(btn5, "30px"); btn5.setLabel("5"); btn5.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '5')"); Button btn6 = new Button(); ZKUpdateUtil.setWidth(btn6, "30px"); btn6.setLabel("6"); btn6.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '6')"); Button btnDivide = new Button(); ZKUpdateUtil.setWidth(btnDivide, "30px"); btnDivide.setLabel("/"); btnDivide.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', ' / ')"); row2.appendChild(btnC); row2.appendChild(btn4); row2.appendChild(btn5); row2.appendChild(btn6); row2.appendChild(btnDivide); Hbox row3 = new Hbox(); Button btnModulo = new Button(); ZKUpdateUtil.setWidth(btnModulo, "40px"); btnModulo.setLabel("%"); btnModulo.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', ' % ')"); Button btn1 = new Button(); ZKUpdateUtil.setWidth(btn1, "30px"); btn1.setLabel("1"); btn1.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '1')"); Button btn2 = new Button(); ZKUpdateUtil.setWidth(btn2, "30px"); btn2.setLabel("2"); btn2.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '2')"); Button btn3 = new Button(); ZKUpdateUtil.setWidth(btn3, "30px"); btn3.setLabel("3"); btn3.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '3')"); Button btnSubstract = new Button(); ZKUpdateUtil.setWidth(btnSubstract, "30px"); btnSubstract.setLabel("-"); btnSubstract.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', ' - ')"); row3.appendChild(btnModulo); row3.appendChild(btn1); row3.appendChild(btn2); row3.appendChild(btn3); row3.appendChild(btnSubstract); Hbox row4 = new Hbox(); Button btnCurrency = new Button(); ZKUpdateUtil.setWidth(btnCurrency, "40px"); btnCurrency.setLabel("$"); btnCurrency.setDisabled(true); Button btn0 = new Button(); ZKUpdateUtil.setWidth(btn0, "30px"); btn0.setLabel("0"); btn0.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '0')"); Button btnDot = new Button(); ZKUpdateUtil.setWidth(btnDot, "30px"); btnDot.setLabel(separator); btnDot.setDisabled(integral); btnDot.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', '" + separator + "')"); Button btnEqual = new Button(); ZKUpdateUtil.setWidth(btnEqual, "30px"); btnEqual.setLabel("="); btnEqual.setWidgetListener("onClick", "calc.evaluate('" + decimalBox.getId() + "','" + txtCalcId + "','" + separator + "')"); Button btnAdd = new Button(); ZKUpdateUtil.setWidth(btnAdd, "30px"); btnAdd.setLabel("+"); btnAdd.setWidgetListener("onClick", "calc.append('" + txtCalcId + "', ' + ')"); row4.appendChild(btnCurrency); row4.appendChild(btnDot); row4.appendChild(btn0); row4.appendChild(btnEqual); row4.appendChild(btnAdd); vbox.appendChild(row1); vbox.appendChild(row2); vbox.appendChild(row3); vbox.appendChild(row4); popup.appendChild(vbox); popup.setWidgetListener("onOpen", "calc.clearAll('" + txtCalcId + "')"); popup.addEventListener(Events.ON_CANCEL, e -> popup.close()); return popup; } /** * @return true if it is for integer, false for decimal number */ public boolean isIntegral() { return integral; } /** * Set integer or decimal number mode * @param integral true for integer mode, false for decimal number mode */ public void setIntegral(boolean integral) { this.integral = integral; if (integral) decimalBox.setScale(0); else decimalBox.setScale(Decimalbox.AUTO); } /** * Set enable/disable. * Hide calculator button if set to disable. * @param enabled */ public void setEnabled(boolean enabled) { decimalBox.setReadonly(!enabled); decimalBox.setDisabled(!enabled); btn.setEnabled(enabled); if (enabled) { if (btn.getParent() != decimalBox.getParent()) btn.setParent(decimalBox.getParent()); } else { if (btn.getParent() != null) btn.detach(); } if (enabled) { LayoutUtils.removeSclass("editor-input-disd", decimalBox); } else { LayoutUtils.addSclass("editor-input-disd", decimalBox); } } /** * @return true if enable, false otherwise */ public boolean isEnabled() { return !decimalBox.isReadonly(); } /** * If evtnm is ON_CLICK, add listener to {@link #btn}. * Otherwise, add listener to {@link #decimalBox}. * @param evtnm * @param listener * @return true if listener added */ @Override public boolean addEventListener(String evtnm, EventListener listener) { if(Events.ON_CLICK.equals(evtnm)) { return btn.addEventListener(evtnm, listener); } else { return decimalBox.addEventListener(evtnm, listener); } } /** * Focus to {@link #decimalBox} */ @Override public void focus() { decimalBox.focus(); } /** * @return decimalBox */ public Decimalbox getDecimalbox() { return decimalBox; } /** @return Button */ public Button getButton() { return btn; } /** * Set to form or grid view mode. * @param flag true for grid view mode, false otherwise */ public void setTableEditorMode(boolean flag) { if (flag) { ZKUpdateUtil.setHflex(this, "0"); LayoutUtils.addSclass("grid-editor-input", decimalBox); LayoutUtils.addSclass("grid-editor-button", btn); } else { ZKUpdateUtil.setHflex(this, "1"); LayoutUtils.removeSclass("grid-editor-input", decimalBox); LayoutUtils.removeSclass("grid-editor-button", btn); } } }