View Javadoc

1   /**********************************************
2    * Copyright (C) 2011 Lukas Laag
3    * This file is part of svgreal.
4    * 
5    * svgreal is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    * 
10   * svgreal is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   * 
15   * You should have received a copy of the GNU General Public License
16   * along with svgreal.  If not, see http://www.gnu.org/licenses/
17   **********************************************/
18  /*
19   * Ext GWT 2.2.5 - Ext for GWT
20   * Copyright(c) 2007-2010, Ext JS, LLC.
21   * licensing@extjs.com
22   * 
23   * http://extjs.com/license
24   */
25  package org.vectomatic.svg.edit.client.gxt.widget;
26  
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  import com.extjs.gxt.ui.client.GXT;
31  import com.extjs.gxt.ui.client.event.ClickRepeaterEvent;
32  import com.extjs.gxt.ui.client.event.ComponentEvent;
33  import com.extjs.gxt.ui.client.event.Events;
34  import com.extjs.gxt.ui.client.event.FieldEvent;
35  import com.extjs.gxt.ui.client.event.Listener;
36  import com.extjs.gxt.ui.client.util.ClickRepeater;
37  import com.extjs.gxt.ui.client.util.Format;
38  import com.extjs.gxt.ui.client.util.KeyNav;
39  import com.extjs.gxt.ui.client.util.Size;
40  import com.extjs.gxt.ui.client.widget.form.NumberPropertyEditor;
41  import com.extjs.gxt.ui.client.widget.form.TwinTriggerField;
42  import com.extjs.gxt.ui.client.widget.form.Validator;
43  import com.google.gwt.dom.client.NativeEvent;
44  import com.google.gwt.i18n.client.LocaleInfo;
45  import com.google.gwt.i18n.client.NumberFormat;
46  import com.google.gwt.i18n.client.constants.NumberConstants;
47  import com.google.gwt.user.client.Element;
48  
49  /**
50   * Extended SpinnerField which provides access to the ClickRepeater
51   * object used to control the spinner buttons. This enables registering
52   * listeners on the ClickRepeater events, in order to deactivate command
53   * generations while the spinner buttons are held pressed (otherwise,
54   * dozens of commands would be generated when one clicks the spinner button
55   * and keeps it pressed).
56   * @author laaglu
57   */
58  public class SpinnerFieldExt extends TwinTriggerField<Number> {
59  
60  	  /**
61  	   * SpinnerField messages.
62  	   */
63  	  public class SpinnerFieldMessages extends TextFieldMessages {
64  	    private String maxText;
65  	    private String minText;
66  	    private String nanText;
67  	    private String negativeText = GXT.MESSAGES.numberField_negativeText();
68  
69  	    /**
70  	     * Returns the max error text.
71  	     * 
72  	     * @return the error text
73  	     */
74  	    public String getMaxText() {
75  	      return maxText;
76  	    }
77  
78  	    /**
79  	     * Returns the minimum error text.
80  	     * 
81  	     * @return the minimum error text
82  	     */
83  	    public String getMinText() {
84  	      return minText;
85  	    }
86  
87  	    /**
88  	     * Returns the not a number error text.
89  	     * 
90  	     * @return the not a number error text
91  	     */
92  	    public String getNanText() {
93  	      return nanText;
94  	    }
95  
96  	    /**
97  	     * Returns the negative error text.
98  	     * 
99  	     * @return the error text
100 	     */
101 	    public String getNegativeText() {
102 	      return negativeText;
103 	    }
104 
105 	    /**
106 	     * Error text to display if the maximum value validation fails (defaults to
107 	     * "The maximum value for this field is {maxValue}").
108 	     * 
109 	     * @param maxText the max error text
110 	     */
111 	    public void setMaxText(String maxText) {
112 	      this.maxText = maxText;
113 	    }
114 
115 	    /**
116 	     * Sets the Error text to display if the minimum value validation fails
117 	     * (defaults to "The minimum value for this field is {minValue}").
118 	     * 
119 	     * @param minText min error text
120 	     */
121 	    public void setMinText(String minText) {
122 	      this.minText = minText;
123 	    }
124 
125 	    /**
126 	     * Sets the error text to display if the value is not a valid number. For
127 	     * example, this can happen if a valid character like '.' or '-' is left in
128 	     * the field with no number (defaults to "{value} is not a valid number").
129 	     * 
130 	     * @param nanText the not a number text
131 	     */
132 	    public void setNanText(String nanText) {
133 	      this.nanText = nanText;
134 	    }
135 
136 	    /**
137 	     * Sets the negative error text (defaults to 'The value must be greater or
138 	     * equal to 0').
139 	     * 
140 	     * @param negativeText the error text
141 	     */
142 	    public void setNegativeText(String negativeText) {
143 	      this.negativeText = negativeText;
144 	    }
145 	  }
146 
147 	  protected List<Character> allowed;
148 	  protected NumberConstants constants;
149 	  protected String decimalSeparator = ".";
150 	  protected KeyNav<ComponentEvent> keyNav;
151 	  private boolean allowDecimals = true;
152 	  private boolean allowNegative = true;
153 	  private String baseChars = "0123456789";
154 	  private Number increment = 1d;
155 	  private int lastKeyCode;
156 	  private Number maxValue = Double.MAX_VALUE;
157 	  private Number minValue = Double.NEGATIVE_INFINITY;
158 	  // begin laaglu
159 	  private ClickRepeater repeater;
160 	  private ClickRepeater twinRepeater;
161 	  // end laaglu
162 
163 	  /**
164 	   * Creates a new number field.
165 	   */
166 	  public SpinnerFieldExt() {
167 	    messages = new SpinnerFieldMessages();
168 	    propertyEditor = new NumberPropertyEditor();
169 	    constants = LocaleInfo.getCurrentLocale().getNumberConstants();
170 	    decimalSeparator = constants.decimalSeparator();
171 	  }
172 
173 	  /**
174 	   * Returns true of decimal values are allowed.
175 	   * 
176 	   * @return the allow decimal state
177 	   */
178 	  public boolean getAllowDecimals() {
179 	    return allowDecimals;
180 	  }
181 
182 	  /**
183 	   * Returns true if negative values are allowed.
184 	   * 
185 	   * @return the allow negative value state
186 	   */
187 	  public boolean getAllowNegative() {
188 	    return allowNegative;
189 	  }
190 
191 	  /**
192 	   * Returns the base characters.
193 	   * 
194 	   * @return the base characters
195 	   */
196 	  public String getBaseChars() {
197 	    return baseChars;
198 	  }
199 
200 	  /**
201 	   * Returns the field's number format.
202 	   * 
203 	   * @return the number format
204 	   */
205 	  public NumberFormat getFormat() {
206 	    return getPropertyEditor().getFormat();
207 	  }
208 
209 	  /**
210 	   * Sets the increment value.
211 	   * 
212 	   * @return the increment
213 	   */
214 	  public Number getIncrement() {
215 	    return increment;
216 	  }
217 
218 	  /**
219 	   * Returns the fields max value.
220 	   * 
221 	   * @return the max value
222 	   */
223 	  public Number getMaxValue() {
224 	    return maxValue;
225 	  }
226 
227 	  @Override
228 	  public SpinnerFieldMessages getMessages() {
229 	    return (SpinnerFieldMessages) messages;
230 	  }
231 
232 	  /**
233 	   * Returns the field's minimum value.
234 	   * 
235 	   * @return the min value
236 	   */
237 	  public Number getMinValue() {
238 	    return minValue;
239 	  }
240 
241 	  @Override
242 	  public NumberPropertyEditor getPropertyEditor() {
243 	    return (NumberPropertyEditor) propertyEditor;
244 	  }
245 
246 	  /**
247 	   * Returns the number property editor number type.
248 	   * 
249 	   * @see NumberPropertyEditor#setType(Class)
250 	   * @return the number type
251 	   */
252 	  public Class<?> getPropertyEditorType() {
253 	    return getPropertyEditor().getType();
254 	  }
255 
256 	  /**
257 	   * Sets whether decimal value are allowed (defaults to true).
258 	   * 
259 	   * @param allowDecimals true to allow negative values
260 	   */
261 	  public void setAllowDecimals(boolean allowDecimals) {
262 	    this.allowDecimals = allowDecimals;
263 	  }
264 
265 	  /**
266 	   * Sets whether negative value are allowed.
267 	   * 
268 	   * @param allowNegative true to allow negative values
269 	   */
270 	  public void setAllowNegative(boolean allowNegative) {
271 	    this.allowNegative = allowNegative;
272 	  }
273 
274 	  /**
275 	   * Sets the base set of characters to evaluate as valid numbers (defaults to
276 	   * '0123456789').
277 	   * 
278 	   * @param baseChars the base character
279 	   */
280 	  public void setBaseChars(String baseChars) {
281 	    assertPreRender();
282 	    this.baseChars = baseChars;
283 	  }
284 
285 	  /**
286 	   * Sets the cell's number formatter.
287 	   * 
288 	   * @param format the format
289 	   */
290 	  public void setFormat(NumberFormat format) {
291 	    getPropertyEditor().setFormat(format);
292 	  }
293 
294 	  /**
295 	   * Sets the increment that should be used (defaults to 1d).
296 	   * 
297 	   * @param increment the increment to set.
298 	   */
299 	  public void setIncrement(Number increment) {
300 	    this.increment = increment;
301 	  }
302 
303 	  /**
304 	   * Sets the field's max allowable value.
305 	   * 
306 	   * @param maxValue the max value
307 	   */
308 	  public void setMaxValue(Number maxValue) {
309 	    this.maxValue = maxValue.doubleValue();
310 	    if (rendered && maxValue.doubleValue() != Double.MAX_VALUE) {
311 	      getInputEl().dom.setAttribute("aria-valuemax", "" + maxValue);
312 	    }
313 	  }
314 
315 	  /**
316 	   * Sets the field's minimum allowed value.
317 	   * 
318 	   * @param minValue the minimum value
319 	   */
320 	  public void setMinValue(Number minValue) {
321 	    this.minValue = minValue.doubleValue();
322 	    if (rendered && maxValue.doubleValue() != Double.NEGATIVE_INFINITY) {
323 	      getInputEl().dom.setAttribute("aria-valuemin", "" + minValue);
324 	    }
325 	  }
326 
327 	  /**
328 	   * Specifies the number type used when converting a String to a Number
329 	   * instance (defaults to Double).
330 	   * 
331 	   * @param type the number type (Short, Integer, Long, Float, Double).
332 	   */
333 	  public void setPropertyEditorType(Class<?> type) {
334 	    getPropertyEditor().setType(type);
335 	  }
336 
337 	  @Override
338 	  protected Size adjustInputSize() {
339 	    return new Size(isHideTrigger() ? 0 : trigger.getStyleSize().width, 0);
340 	  }
341 
342 	  protected void afterRender() {
343 	    super.afterRender();
344 	    addStyleOnOver(trigger.dom, "x-form-spinner-overup");
345 	    addStyleOnOver(twinTrigger.dom, "x-form-spinner-overdown");
346 	  }
347 
348 	  protected void doSpin(boolean up) {
349 	    if (!readOnly) {
350 	      Number n = getValue();
351 	      double d = n == null ? 0d : getValue().doubleValue();
352 	      if (up) {
353 	        setValue(Math.max(minValue.doubleValue(), Math.min(d + increment.doubleValue(), maxValue.doubleValue())));
354 	      } else {
355 	        setValue(Math.max(
356 	            minValue.doubleValue(),
357 	            Math.min(allowNegative ? d - increment.doubleValue() : Math.max(0, d - increment.doubleValue()),
358 	                maxValue.doubleValue())));
359 	      }
360 	    }
361 	  }
362 
363 	  @Override
364 	  protected void onKeyDown(FieldEvent fe) {
365 	    super.onKeyDown(fe);
366 	    // must key code in key code as character returned in key press
367 	    lastKeyCode = getKeyCode(fe.getEvent());
368 	  }
369 
370 	  @Override
371 	  protected void onKeyPress(FieldEvent fe) {
372 	    super.onKeyPress(fe);
373 	    char key = getChar(fe.getEvent());
374 
375 	    if (fe.isSpecialKey(lastKeyCode) || fe.isControlKey()) {
376 	      return;
377 	    }
378 
379 	    if (!allowed.contains(key)) {
380 	      fe.stopEvent();
381 	    }
382 	  }
383 
384 	  @Override
385 	  protected void onRender(Element target, int index) {
386 	    super.onRender(target, index);
387 	    allowed = new ArrayList<Character>();
388 	    for (int i = 0; i < baseChars.length(); i++) {
389 	      allowed.add(baseChars.charAt(i));
390 	    }
391 
392 	    if (allowNegative) {
393 	      allowed.add('-');
394 	    }
395 	    if (allowDecimals) {
396 	      for (int i = 0; i < decimalSeparator.length(); i++) {
397 	        allowed.add(decimalSeparator.charAt(i));
398 	      }
399 	    }
400 
401 	    Listener<ClickRepeaterEvent> listener = new Listener<ClickRepeaterEvent>() {
402 	      public void handleEvent(ClickRepeaterEvent be) {
403 	        if (SpinnerFieldExt.this.isEnabled()) {
404 	          if (!hasFocus) {
405 	            focus();
406 	          }
407 	          if (be.getType() == Events.OnClick) {
408 	            if (be.getEl() == trigger) {
409 	              onTriggerClick(null);
410 	            } else if (be.getEl() == twinTrigger) {
411 	              onTwinTriggerClick(null);
412 	            }
413 	          } else if (be.getType() == Events.OnMouseDown) {
414 	            if (be.getEl() == trigger) {
415 	              trigger.addStyleName("x-form-spinner-clickup");
416 	            } else if (be.getEl() == twinTrigger) {
417 	              twinTrigger.addStyleName("x-form-spinner-clickdown");
418 	            }
419 
420 	          } else if (be.getType() == Events.OnMouseUp) {
421 	            if (be.getEl() == trigger) {
422 	              trigger.removeStyleName("x-form-spinner-clickup");
423 	            } else if (be.getEl() == twinTrigger) {
424 	              twinTrigger.removeStyleName("x-form-spinner-clickdown");
425 	            }
426 	          }
427 	        }
428 	      }
429 	    };
430 
431 	    repeater = new ClickRepeater(trigger);
432 	    repeater.addListener(Events.OnClick, listener);
433 	    repeater.addListener(Events.OnMouseDown, listener);
434 	    repeater.addListener(Events.OnMouseUp, listener);
435 	    addAttachable(repeater);
436 
437 	    twinRepeater = new ClickRepeater(twinTrigger);
438 	    twinRepeater.addListener(Events.OnClick, listener);
439 	    twinRepeater.addListener(Events.OnMouseDown, listener);
440 	    twinRepeater.addListener(Events.OnMouseUp, listener);
441 	    addAttachable(twinRepeater);
442 
443 	    addStyleName("x-spinner-field");
444 	    trigger.addStyleName("x-form-spinner-up");
445 	    twinTrigger.addStyleName("x-form-spinner-down");
446 
447 	    setMaxValue(maxValue);
448 	    setMinValue(minValue);
449 	    getInputEl().dom.setAttribute("role", "spinbutton");
450 
451 	    keyNav = new KeyNav<ComponentEvent>(this) {
452 	      @Override
453 	      public void onDown(ComponentEvent ce) {
454 	        doSpin(false);
455 	      }
456 
457 	      @Override
458 	      public void onUp(ComponentEvent ce) {
459 	        doSpin(true);
460 	      }
461 	    };
462 	  }
463 
464 	  protected void onTriggerClick(ComponentEvent ce) {
465 	    super.onTriggerClick(ce);
466 	    // only do it from the ClickRepeater, not from onBrowserEvent
467 	    if (ce == null) {
468 	      doSpin(true);
469 	    }
470 	  }
471 
472 	  protected void onTwinTriggerClick(ComponentEvent ce) {
473 	    super.onTwinTriggerClick(ce);
474 	    // only do it from the ClickRepeater, not from onBrowserEvent
475 	    if (ce == null) {
476 	      doSpin(false);
477 	    }
478 	  }
479 
480 	  @Override
481 	  protected boolean validateValue(String value) {
482 	    // validator should run after super rules
483 	    Validator tv = validator;
484 	    validator = null;
485 	    if (!super.validateValue(value)) {
486 	      validator = tv;
487 	      return false;
488 	    }
489 	    validator = tv;
490 	    if (value.length() < 1) { // if it's blank and textfield didn't flag it then
491 	      // its valid it's valid
492 	      return true;
493 	    }
494 
495 	    String v = value;
496 
497 	    Number d = null;
498 	    try {
499 	      d = getPropertyEditor().convertStringValue(v);
500 	    } catch (Exception e) {
501 	      String error = "";
502 	      if (getMessages().getNanText() == null) {
503 	        error = GXT.MESSAGES.numberField_nanText(v);
504 	      } else {
505 	        error = Format.substitute(getMessages().getNanText(), v);
506 	      }
507 	      markInvalid(error);
508 	      return false;
509 	    }
510 	    if (d.doubleValue() < minValue.doubleValue()) {
511 	      String error = "";
512 	      if (getMessages().getMinText() == null) {
513 	        error = GXT.MESSAGES.numberField_minText(minValue.doubleValue());
514 	      } else {
515 	        error = Format.substitute(getMessages().getMinText(), minValue);
516 	      }
517 	      markInvalid(error);
518 	      return false;
519 	    }
520 
521 	    if (d.doubleValue() > maxValue.doubleValue()) {
522 	      String error = "";
523 	      if (getMessages().getMaxText() == null) {
524 	        error = GXT.MESSAGES.numberField_maxText(maxValue.doubleValue());
525 	      } else {
526 	        error = Format.substitute(getMessages().getMaxText(), maxValue);
527 	      }
528 	      markInvalid(error);
529 	      return false;
530 	    }
531 
532 	    if (!allowNegative && d.doubleValue() < 0) {
533 	      markInvalid(getMessages().getNegativeText());
534 	      return false;
535 	    }
536 
537 	    if (validator != null) {
538 	      String msg = validator.validate(this, value);
539 	      if (msg != null) {
540 	        markInvalid(msg);
541 	        return false;
542 	      }
543 	    }
544 
545 	    if (GXT.isAriaEnabled()) {
546 	      getInputEl().dom.setAttribute("aria-valuenow", "" + value);
547 	    }
548 
549 	    return true;
550 	  }
551 
552 	  // needed due to GWT 2.1 changes
553 	  private native char getChar(NativeEvent e) /*-{
554 			return e.which || e.charCode || e.keyCode || 0;
555 	  }-*/;
556 
557 	  // needed due to GWT 2.1 changes
558 	  private native int getKeyCode(NativeEvent e) /*-{
559 			return e.keyCode || 0;
560 	  }-*/;
561 
562 	  // begin laaglu
563 	  /**
564 	   * Returns the repeater used to control the trigger
565 	   */
566 	  public ClickRepeater getRepeater() {
567 		  return repeater;
568 	  }
569 	  /**
570 	   * Returns the repeater used to control the twin trigger
571 	   */
572 	  public ClickRepeater getTwinRepeater() {
573 		  return twinRepeater;
574 	  }
575 	  // end laaglu
576 }