--- /dev/null
+@mixin base-common {
+
+/* References the BODY tag generated by Vaadin application servlet */
+.v-generated-body {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ margin: 0;
+ overflow: hidden;
+}
+.v-app {
+ height: 100%;
+}
+/* Force arrow cursor for all elements inside the app */
+.v-app,
+.v-window,
+.v-popupview-popup,
+.v-label,
+.v-caption {
+ cursor: default;
+}
+div.v-app-loading {
+ /* You can use this to provide indication for the user that the application is loading. */
+ /* It is applied to the same element as .v-app */
+ background-image: url(img/loading-indicator.gif);
+ background-repeat: no-repeat;
+ background-position: 50%;
+ width: 100%;
+ height: 100%;
+}
+.v-view {
+ height: 100%;
+ width: 100%;
+ /* avoid scrollbars with margins in root layout */
+ outline: none;
+ position: relative;
+}
+/* Prevent margin collapse */
+.v-view.v-view-embedded {
+ margin-top: -1px;
+ border-top: 1px solid transparent;
+}
+/**
+ * Try to handle printing somehow. Reasonable printing support
+ * needs application specific planning and CSS tuning.
+ */
+@media print {
+ .v-generated-body {
+ height: auto;
+ min-height: 20cm;
+ overflow: visible;
+ }
+ .v-app {
+ height: auto;
+ min-height: 20cm;
+ }
+ .v-view {
+ overflow: visible;
+ }
+ .v-gridlayout {
+ overflow: visible !important;
+ }
+}
+.v-view:active,
+.v-view:focus {
+ outline: none;
+}
+.v-app select,
+.v-window select {
+ margin: 0;
+}
+.v-disabled {
+ opacity: .3;
+ filter: alpha(opacity=30);
+ cursor: default;
+}
+.v-disabled * {
+ cursor: default;
+}
+* html .v-disabled {
+ zoom: 1;
+}
+*+html .v-disabled {
+ zoom: 1;
+}
+.v-disabled .v-disabled {
+ opacity: 1;
+}
+.v-required-field-indicator {
+ padding-left: 2px;
+ color: red;
+}
+.v-form fieldset {
+ border: none;
+ padding: 0;
+ margin: 0;
+ height: 100%;
+}
+.v-form-content {
+ height: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/* Field modified */ /* Disabled by default
+.v-modified,
+.v-richtextarea.v-modified iframe.gwt-RichTextArea,
+.v-checkbox.v-modified,
+.v-modified .v-select-option,
+.v-modified .v-textfield,
+.v-modified .v-datefield-calendarpanel,
+.v-modified .v-select-select,
+.v-modified .v-select-twincol-selections,
+.v-modified .v-select-twincol-options,
+.v-modified .v-slider-base {
+ background: #ffffe0;
+}
+*/
+.v-tooltip {
+ cursor: default;
+ background: #fff;
+}
+.v-tooltip-text {
+ overflow: auto;
+}
+.v-tooltip .v-errormessage {
+ overflow: auto;
+}
+.v-contextmenu {
+ background: #fff;
+}
+.v-contextmenu .gwt-MenuItem {
+ cursor: pointer;
+ vertical-align: middle;
+ padding: 0;
+ border: 0;
+ margin: 0;
+}
+.v-contextmenu .gwt-MenuItem div {
+ cursor: pointer;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+.v-contextmenu .gwt-MenuItem-selected div {
+ background: #aaa;
+ color: #fff;
+}
+.v-contextmenu table {
+ border-collapse: collapse;
+ margin: 0;
+ padding: 0;
+}
+.v-contextmenu .gwt-MenuItem img {
+ margin-right: 1em;
+ vertical-align: middle;
+}
+/* Margins are not supported within Label */
+.v-label pre {
+ margin: 0;
+}
+/* A label with undefined width is always considered to be on one line */
+.v-label-undef-w {
+ white-space: nowrap;
+}
+/* Revert line-height for heading elements inside labels */
+.v-label h1,
+.v-label h2,
+.v-label h3,
+.v-label h4,
+.v-label h5,
+.v-label h6 {
+ line-height: normal;
+}
+/* Loading indicator states
+ * Note: client side expects that loading indicator has a height. It depends on
+ * this css property to ensure browsers have applied all required styles.
+ */
+.v-loading-indicator,
+.v-loading-indicator-delay,
+.v-loading-indicator-wait {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 30000;
+ width: 31px;
+ height: 31px;
+ background: transparent url(img/loading-indicator.gif);
+ margin-right: 5px;
+ margin-top: 5px;
+}
+.v-loading-indicator-delay {
+ background-image: url(img/loading-indicator-delay.gif);
+}
+.v-loading-indicator-wait {
+ background-image: url(img/loading-indicator-wait.gif);
+}
+/* Debug dialog */
+.v-debug-console {
+ background: #fff;
+ opacity: .9;
+ border: 1px solid #000;
+ font-family: sans-serif;
+}
+.v-debug-console-caption {
+ background: #000;
+ border-bottom: 1px solid grey;
+ color: white;
+ font-weight: bold;
+}
+.v-debug-console-content {
+ font-size: x-small;
+ overflow: auto;
+ white-space: pre;
+}
+.v-debug-console-content input {
+ font-size: xx-small;
+}
+/* Debug style */
+.v-app .invalidlayout,
+.v-app .invalidlayout * {
+ background: #f99 !important;
+}
+/* Fix for Liferay, issue #2384 */
+.v-app input[type="text"],
+.v-app input[type="password"],
+.v-app input[type="reset"],
+.v-app select,
+.v-app textarea ,
+.v-window input[type="text"],
+.v-window input[type="password"],
+.v-window input[type="reset"],
+.v-window select,
+.v-window textarea {
+ padding: 2px;
+}
+
+.v-drag-element {
+ z-index: 60000;
+ /* override any other position: properties */
+ position: absolute !important;
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+ cursor: default;
+}
+
+.v-clip {
+ overflow: hidden;
+}
+
+.v-scrollable {
+ overflow: auto;
+}
+
++/* Enable kinetic scrolling on Mobile Safari 6 */
++.v-ios.v-sa6 .v-scrollable {
++ -webkit-overflow-scrolling: touch;
+}
--- /dev/null
+/*
+ * Copyright 2011 Vaadin Ltd.
+ *
+ * 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 com.vaadin.tools;
+
+import java.lang.reflect.Method;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.server.widgetsetutils.WidgetSetBuilder;
+
+/**
+ * A wrapper for the GWT 1.6 compiler that runs the compiler in a new thread.
+ *
+ * This allows circumventing a J2SE 5.0 bug (6316197) that prevents setting the
+ * stack size for the main thread. Thus, larger widgetsets can be compiled.
+ *
+ * This class takes the same command line arguments as the
+ * com.google.gwt.dev.GWTCompiler class. The old and deprecated compiler is used
+ * for compatibility with GWT 1.5.
+ *
+ * A typical invocation would use e.g. the following arguments
+ *
+ * "-out WebContent/VAADIN/widgetsets com.vaadin.DefaultWidgetSet"
+ *
+ * In addition, larger memory usage settings for the VM should be used, e.g.
+ *
+ * "-Xms256M -Xmx512M -Xss8M"
+ *
+ * The source directory containing widgetset and related classes must be
+ * included in the classpath, as well as the gwt-dev-[platform].jar and other
+ * relevant JARs.
+ *
+ * @deprecated with Java 6, can use com.google.gwt.dev.Compiler directly (also
+ * in Eclipse plug-in etc.)
+ */
+@Deprecated
+public class WidgetsetCompiler {
+
+ /**
+ * @param args
+ * same arguments as for com.google.gwt.dev.Compiler
+ */
+ public static void main(final String[] args) {
+ try {
+ // run the compiler in a different thread to enable using the
+ // user-set stack size
+
+ // on Windows, the default stack size is too small for the main
+ // thread and cannot be changed in JRE 1.5 (see
+ // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6316197)
+
+ Runnable runCompiler = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // GWTCompiler.main(args);
+ // avoid warnings
+
+ String wsname = args[args.length - 1];
+
+ // TODO expecting this is launched via eclipse WTP
+ // project
+ System.out
+ .println("Updating GWT module description file...");
+ WidgetSetBuilder.updateWidgetSet(wsname);
+ System.out.println("Done.");
+
+ System.out.println("Starting GWT compiler");
+ System.setProperty("gwt.nowarn.legacy.tools", "true");
++ System.setProperty("gwt.forceVersionCheckURL",
++ "http://tools.vaadin.com/version/currentversion.xml");
+ Class<?> compilerClass = Class
+ .forName("com.google.gwt.dev.GWTCompiler");
+ Method method = compilerClass.getDeclaredMethod("main",
+ String[].class);
+ method.invoke(null, new Object[] { args });
+ } catch (Throwable thr) {
+ getLogger().log(Level.SEVERE,
+ "Widgetset compilation failed", thr);
+ }
+ }
+ };
+ Thread runThread = new Thread(runCompiler);
+ runThread.start();
+ runThread.join();
+ System.out.println("Widgetset compilation finished");
+ } catch (Throwable thr) {
+ getLogger().log(Level.SEVERE, "Widgetset compilation failed", thr);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(WidgetsetCompiler.class.getName());
+ }
+}
--- /dev/null
- // Cannot enable native touch scrolling on iOS 5 until #8792 is resolved
- // if (isIOS() && isWebkit() && getWebkitVersion() >= 534) {
- // return false;
- // }
+/*
+ * Copyright 2011 Vaadin Ltd.
+ *
+ * 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 com.vaadin.client;
+
+import com.google.gwt.user.client.ui.RootPanel;
+import com.vaadin.shared.VBrowserDetails;
+
+/**
+ * Class used to query information about web browser.
+ *
+ * Browser details are detected only once and those are stored in this singleton
+ * class.
+ *
+ */
+public class BrowserInfo {
+
+ private static final String BROWSER_OPERA = "op";
+ private static final String BROWSER_IE = "ie";
+ private static final String BROWSER_FIREFOX = "ff";
+ private static final String BROWSER_SAFARI = "sa";
+
+ public static final String ENGINE_GECKO = "gecko";
+ public static final String ENGINE_WEBKIT = "webkit";
+ public static final String ENGINE_PRESTO = "presto";
+ public static final String ENGINE_TRIDENT = "trident";
+
+ private static final String OS_WINDOWS = "win";
+ private static final String OS_LINUX = "lin";
+ private static final String OS_MACOSX = "mac";
+ private static final String OS_ANDROID = "android";
+ private static final String OS_IOS = "ios";
+
+ // Common CSS class for all touch devices
+ private static final String UI_TOUCH = "touch";
+
+ private static BrowserInfo instance;
+
+ private static String cssClass = null;
+
+ static {
+ // Add browser dependent v-* classnames to body to help css hacks
+ String browserClassnames = get().getCSSClass();
+ RootPanel.get().addStyleName(browserClassnames);
+ }
+
+ /**
+ * Singleton method to get BrowserInfo object.
+ *
+ * @return instance of BrowserInfo object
+ */
+ public static BrowserInfo get() {
+ if (instance == null) {
+ instance = new BrowserInfo();
+ }
+ return instance;
+ }
+
+ private VBrowserDetails browserDetails;
+ private boolean touchDevice;
+
+ private BrowserInfo() {
+ browserDetails = new VBrowserDetails(getBrowserString());
+ if (browserDetails.isIE()) {
+ // Use document mode instead user agent to accurately detect how we
+ // are rendering
+ int documentMode = getIEDocumentMode();
+ if (documentMode != -1) {
+ browserDetails.setIEMode(documentMode);
+ }
+ }
+
+ if (browserDetails.isChrome()) {
+ touchDevice = detectChromeTouchDevice();
+ } else {
+ touchDevice = detectTouchDevice();
+ }
+ }
+
+ private native boolean detectTouchDevice()
+ /*-{
+ try { document.createEvent("TouchEvent");return true;} catch(e){return false;};
+ }-*/;
+
+ private native boolean detectChromeTouchDevice()
+ /*-{
+ return ("ontouchstart" in window);
+ }-*/;
+
+ private native int getIEDocumentMode()
+ /*-{
+ var mode = $wnd.document.documentMode;
+ if (!mode)
+ return -1;
+ return mode;
+ }-*/;
+
+ /**
+ * Returns a string representing the browser in use, for use in CSS
+ * classnames. The classnames will be space separated abbreviations,
+ * optionally with a version appended.
+ *
+ * Abbreviations: Firefox: ff Internet Explorer: ie Safari: sa Opera: op
+ *
+ * Browsers that CSS-wise behave like each other will get the same
+ * abbreviation (this usually depends on the rendering engine).
+ *
+ * This is quite simple at the moment, more heuristics will be added when
+ * needed.
+ *
+ * Examples: Internet Explorer 6: ".v-ie .v-ie6 .v-ie60", Firefox 3.0.4:
+ * ".v-ff .v-ff3 .v-ff30", Opera 9.60: ".v-op .v-op9 .v-op960", Opera 10.10:
+ * ".v-op .v-op10 .v-op1010"
+ *
+ * @return
+ */
+ public String getCSSClass() {
+ String prefix = "v-";
+
+ if (cssClass == null) {
+ String browserIdentifier = "";
+ String majorVersionClass = "";
+ String minorVersionClass = "";
+ String browserEngineClass = "";
+
+ if (browserDetails.isFirefox()) {
+ browserIdentifier = BROWSER_FIREFOX;
+ majorVersionClass = browserIdentifier
+ + getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_GECKO;
+ } else if (browserDetails.isChrome()) {
+ // TODO update when Chrome is more stable
+ browserIdentifier = BROWSER_SAFARI;
+ majorVersionClass = "ch";
+ browserEngineClass = ENGINE_WEBKIT;
+ } else if (browserDetails.isSafari()) {
+ browserIdentifier = BROWSER_SAFARI;
+ majorVersionClass = browserIdentifier
+ + getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_WEBKIT;
+ } else if (browserDetails.isIE()) {
+ browserIdentifier = BROWSER_IE;
+ majorVersionClass = browserIdentifier
+ + getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_TRIDENT;
+ } else if (browserDetails.isOpera()) {
+ browserIdentifier = BROWSER_OPERA;
+ majorVersionClass = browserIdentifier
+ + getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_PRESTO;
+ }
+
+ cssClass = prefix + browserIdentifier;
+ if (!"".equals(majorVersionClass)) {
+ cssClass = cssClass + " " + prefix + majorVersionClass;
+ }
+ if (!"".equals(minorVersionClass)) {
+ cssClass = cssClass + " " + prefix + minorVersionClass;
+ }
+ if (!"".equals(browserEngineClass)) {
+ cssClass = cssClass + " " + prefix + browserEngineClass;
+ }
+ String osClass = getOperatingSystemClass();
+ if (osClass != null) {
+ cssClass = cssClass + " " + prefix + osClass;
+ }
+ if (isTouchDevice()) {
+ cssClass = cssClass + " " + prefix + UI_TOUCH;
+ }
+ }
+
+ return cssClass;
+ }
+
+ private String getOperatingSystemClass() {
+ if (browserDetails.isAndroid()) {
+ return OS_ANDROID;
+ } else if (browserDetails.isIOS()) {
+ return OS_IOS;
+ } else if (browserDetails.isWindows()) {
+ return OS_WINDOWS;
+ } else if (browserDetails.isLinux()) {
+ return OS_LINUX;
+ } else if (browserDetails.isMacOSX()) {
+ return OS_MACOSX;
+ }
+ // Unknown OS
+ return null;
+ }
+
+ public boolean isIE() {
+ return browserDetails.isIE();
+ }
+
+ public boolean isFirefox() {
+ return browserDetails.isFirefox();
+ }
+
+ public boolean isSafari() {
+ return browserDetails.isSafari();
+ }
+
+ public boolean isIE8() {
+ return isIE() && getBrowserMajorVersion() == 8;
+ }
+
+ public boolean isIE9() {
+ return isIE() && getBrowserMajorVersion() == 9;
+ }
+
+ public boolean isChrome() {
+ return browserDetails.isChrome();
+ }
+
+ public boolean isGecko() {
+ return browserDetails.isGecko();
+ }
+
+ public boolean isWebkit() {
+ return browserDetails.isWebKit();
+ }
+
+ /**
+ * Returns the Gecko version if the browser is Gecko based. The Gecko
+ * version for Firefox 2 is 1.8 and 1.9 for Firefox 3.
+ *
+ * @return The Gecko version or -1 if the browser is not Gecko based
+ */
+ public float getGeckoVersion() {
+ if (!browserDetails.isGecko()) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserEngineVersion();
+ }
+
+ /**
+ * Returns the WebKit version if the browser is WebKit based. The WebKit
+ * version returned is the major version e.g., 523.
+ *
+ * @return The WebKit version or -1 if the browser is not WebKit based
+ */
+ public float getWebkitVersion() {
+ if (!browserDetails.isWebKit()) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserEngineVersion();
+ }
+
+ public float getIEVersion() {
+ if (!browserDetails.isIE()) {
+ return -1;
+ }
+
+ return getBrowserMajorVersion();
+ }
+
+ public float getOperaVersion() {
+ if (!browserDetails.isOpera()) {
+ return -1;
+ }
+
+ return getBrowserMajorVersion();
+ }
+
+ public boolean isOpera() {
+ return browserDetails.isOpera();
+ }
+
+ public boolean isOpera10() {
+ return browserDetails.isOpera() && getBrowserMajorVersion() == 10;
+ }
+
+ public boolean isOpera11() {
+ return browserDetails.isOpera() && getBrowserMajorVersion() == 11;
+ }
+
+ public native static String getBrowserString()
+ /*-{
+ return $wnd.navigator.userAgent;
+ }-*/;
+
+ public native int getScreenWidth()
+ /*-{
+ return $wnd.screen.width;
+ }-*/;
+
+ public native int getScreenHeight()
+ /*-{
+ return $wnd.screen.height;
+ }-*/;
+
+ /**
+ * @return true if the browser runs on a touch based device.
+ */
+ public boolean isTouchDevice() {
+ return touchDevice;
+ }
+
+ /**
+ * Indicates whether the browser might require juggling to properly update
+ * sizes inside elements with overflow: auto.
+ *
+ * @return <code>true</code> if the browser requires the workaround,
+ * otherwise <code>false</code>
+ */
+ public boolean requiresOverflowAutoFix() {
+ return (getWebkitVersion() > 0 || getOperaVersion() >= 11)
+ && Util.getNativeScrollbarSize() > 0;
+ }
+
+ /**
+ * Checks if the browser is run on iOS
+ *
+ * @return true if the browser is run on iOS, false otherwise
+ */
+ public boolean isIOS() {
+ return browserDetails.isIOS();
+ }
+
+ /**
+ * Checks if the browser is run on Android
+ *
+ * @return true if the browser is run on Android, false otherwise
+ */
+ public boolean isAndroid() {
+ return browserDetails.isAndroid();
+ }
+
+ /**
+ * Checks if the browser is capable of handling scrolling natively or if a
+ * touch scroll helper is needed for scrolling.
+ *
+ * @return true if browser needs a touch scroll helper, false if the browser
+ * can handle scrolling natively
+ */
+ public boolean requiresTouchScrollDelegate() {
+ if (!isTouchDevice()) {
+ return false;
+ }
++ // TODO Should test other Android browsers, especially Chrome
+ if (isAndroid() && isWebkit() && getWebkitVersion() >= 534) {
+ return false;
+ }
++ // iOS 6 Safari supports native scrolling; iOS 5 suffers from #8792
++ // TODO Should test other iOS browsers
++ if (isIOS() && isSafari() && getBrowserMajorVersion() >= 6) {
++ return false;
++ }
+ return true;
+ }
+
+ /**
+ * Tests if this is an Android devices with a broken scrollTop
+ * implementation
+ *
+ * @return true if scrollTop cannot be trusted on this device, false
+ * otherwise
+ */
+ public boolean isAndroidWithBrokenScrollTop() {
+ return isAndroid()
+ && (getOperatingSystemMajorVersion() == 3 || getOperatingSystemMajorVersion() == 4);
+ }
+
+ private int getOperatingSystemMajorVersion() {
+ return browserDetails.getOperatingSystemMajorVersion();
+ }
+
+ /**
+ * Returns the browser major version e.g., 3 for Firefox 3.5, 4 for Chrome
+ * 4, 8 for Internet Explorer 8.
+ * <p>
+ * Note that Internet Explorer 8 and newer will return the document mode so
+ * IE8 rendering as IE7 will return 7.
+ * </p>
+ *
+ * @return The major version of the browser.
+ */
+ public int getBrowserMajorVersion() {
+ return browserDetails.getBrowserMajorVersion();
+ }
+
+ /**
+ * Returns the browser minor version e.g., 5 for Firefox 3.5.
+ *
+ * @see #getBrowserMajorVersion()
+ *
+ * @return The minor version of the browser, or -1 if not known/parsed.
+ */
+ public int getBrowserMinorVersion() {
+ return browserDetails.getBrowserMinorVersion();
+ }
+
+ /**
+ * Checks if the browser version is newer or equal to the given major+minor
+ * version.
+ *
+ * @param majorVersion
+ * The major version to check for
+ * @param minorVersion
+ * The minor version to check for
+ * @return true if the browser version is newer or equal to the given
+ * version
+ */
+ public boolean isBrowserVersionNewerOrEqual(int majorVersion,
+ int minorVersion) {
+ if (getBrowserMajorVersion() == majorVersion) {
+ // Same major
+ return (getBrowserMinorVersion() >= minorVersion);
+ }
+
+ // Older or newer major
+ return (getBrowserMajorVersion() > majorVersion);
+ }
+}
--- /dev/null
- Day day = (Day) event.getSource();
- focusDay(day.getDate());
+/*
+ * Copyright 2011 Vaadin Ltd.
+ *
+ * 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 com.vaadin.client.ui.datefield;
+
+import java.util.Date;
+import java.util.Iterator;
+
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.DateTimeService;
+import com.vaadin.client.Util;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.ui.FocusableFlexTable;
+import com.vaadin.client.ui.SubPartAware;
+import com.vaadin.client.ui.label.VLabel;
+import com.vaadin.client.ui.nativeselect.VNativeSelect;
+
+@SuppressWarnings("deprecation")
+public class VCalendarPanel extends FocusableFlexTable implements
+ KeyDownHandler, KeyPressHandler, MouseOutHandler, MouseDownHandler,
+ MouseUpHandler, BlurHandler, FocusHandler, SubPartAware {
+
+ public interface SubmitListener {
+
+ /**
+ * Called when calendar user triggers a submitting operation in calendar
+ * panel. Eg. clicking on day or hitting enter.
+ */
+ void onSubmit();
+
+ /**
+ * On eg. ESC key.
+ */
+ void onCancel();
+ }
+
+ /**
+ * Blur listener that listens to blur event from the panel
+ */
+ public interface FocusOutListener {
+ /**
+ * @return true if the calendar panel is not used after focus moves out
+ */
+ boolean onFocusOut(DomEvent<?> event);
+ }
+
+ /**
+ * FocusChangeListener is notified when the panel changes its _focused_
+ * value.
+ */
+ public interface FocusChangeListener {
+ void focusChanged(Date focusedDate);
+ }
+
+ /**
+ * Dispatches an event when the panel when time is changed
+ */
+ public interface TimeChangeListener {
+
+ void changed(int hour, int min, int sec, int msec);
+ }
+
+ /**
+ * Represents a Date button in the calendar
+ */
+ private class VEventButton extends Button {
+ public VEventButton() {
+ addMouseDownHandler(VCalendarPanel.this);
+ addMouseOutHandler(VCalendarPanel.this);
+ addMouseUpHandler(VCalendarPanel.this);
+ }
+ }
+
+ private static final String CN_FOCUSED = "focused";
+
+ private static final String CN_TODAY = "today";
+
+ private static final String CN_SELECTED = "selected";
+
+ private static final String CN_OFFMONTH = "offmonth";
+
+ /**
+ * Represents a click handler for when a user selects a value by using the
+ * mouse
+ */
+ private ClickHandler dayClickHandler = new ClickHandler() {
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt
+ * .event.dom.client.ClickEvent)
+ */
+ @Override
+ public void onClick(ClickEvent event) {
- focusedDate.getMonth()) : "";
- final int year = focusedDate.getYear() + 1900;
++ Date newDate = ((Day) event.getSource()).getDate();
++ if (newDate.getMonth() != displayedMonth.getMonth()
++ || newDate.getYear() != displayedMonth.getYear()) {
++ // If an off-month date was clicked, we must change the
++ // displayed month and re-render the calendar (#8931)
++ displayedMonth.setMonth(newDate.getMonth());
++ displayedMonth.setYear(newDate.getYear());
++ renderCalendar();
++ }
++ focusDay(newDate);
+ selectFocused();
+ onSubmit();
+ }
+ };
+
+ private VEventButton prevYear;
+
+ private VEventButton nextYear;
+
+ private VEventButton prevMonth;
+
+ private VEventButton nextMonth;
+
+ private VTime time;
+
+ private FlexTable days = new FlexTable();
+
+ private int resolution = VDateField.RESOLUTION_YEAR;
+
+ private int focusedRow;
+
+ private Timer mouseTimer;
+
+ private Date value;
+
+ private boolean enabled = true;
+
+ private boolean readonly = false;
+
+ private DateTimeService dateTimeService;
+
+ private boolean showISOWeekNumbers;
+
+ private Date displayedMonth;
+
+ private Date focusedDate;
+
+ private Day selectedDay;
+
+ private Day focusedDay;
+
+ private FocusOutListener focusOutListener;
+
+ private SubmitListener submitListener;
+
+ private FocusChangeListener focusChangeListener;
+
+ private TimeChangeListener timeChangeListener;
+
+ private boolean hasFocus = false;
+
+ public VCalendarPanel() {
+
+ setStyleName(VDateField.CLASSNAME + "-calendarpanel");
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ addKeyPressHandler(this);
+ } else {
+ addKeyDownHandler(this);
+ }
+ addFocusHandler(this);
+ addBlurHandler(this);
+
+ }
+
+ /**
+ * Sets the focus to given date in the current view. Used when moving in the
+ * calendar with the keyboard.
+ *
+ * @param date
+ * A Date representing the day of month to be focused. Must be
+ * one of the days currently visible.
+ */
+ private void focusDay(Date date) {
+ // Only used when calender body is present
+ if (resolution > VDateField.RESOLUTION_MONTH) {
+ if (focusedDay != null) {
+ focusedDay.removeStyleDependentName(CN_FOCUSED);
+ }
+
+ if (date != null && focusedDate != null) {
+ focusedDate.setTime(date.getTime());
+ int rowCount = days.getRowCount();
+ for (int i = 0; i < rowCount; i++) {
+ int cellCount = days.getCellCount(i);
+ for (int j = 0; j < cellCount; j++) {
+ Widget widget = days.getWidget(i, j);
+ if (widget != null && widget instanceof Day) {
+ Day curday = (Day) widget;
+ if (curday.getDate().equals(date)) {
+ curday.addStyleDependentName(CN_FOCUSED);
+ focusedDay = curday;
+ focusedRow = i;
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the selection highlight to a given day in the current view
+ *
+ * @param date
+ * A Date representing the day of month to be selected. Must be
+ * one of the days currently visible.
+ *
+ */
+ private void selectDate(Date date) {
+ if (selectedDay != null) {
+ selectedDay.removeStyleDependentName(CN_SELECTED);
+ }
+
+ int rowCount = days.getRowCount();
+ for (int i = 0; i < rowCount; i++) {
+ int cellCount = days.getCellCount(i);
+ for (int j = 0; j < cellCount; j++) {
+ Widget widget = days.getWidget(i, j);
+ if (widget != null && widget instanceof Day) {
+ Day curday = (Day) widget;
+ if (curday.getDate().equals(date)) {
+ curday.addStyleDependentName(CN_SELECTED);
+ selectedDay = curday;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates year, month, day from focusedDate to value
+ */
+ private void selectFocused() {
+ if (focusedDate != null) {
+ if (value == null) {
+ // No previously selected value (set to null on server side).
+ // Create a new date using current date and time
+ value = new Date();
+ }
+ /*
+ * #5594 set Date (day) to 1 in order to prevent any kind of
+ * wrapping of months when later setting the month. (e.g. 31 ->
+ * month with 30 days -> wraps to the 1st of the following month,
+ * e.g. 31st of May -> 31st of April = 1st of May)
+ */
+ value.setDate(1);
+ if (value.getYear() != focusedDate.getYear()) {
+ value.setYear(focusedDate.getYear());
+ }
+ if (value.getMonth() != focusedDate.getMonth()) {
+ value.setMonth(focusedDate.getMonth());
+ }
+ if (value.getDate() != focusedDate.getDate()) {
+ }
+ // We always need to set the date, even if it hasn't changed, since
+ // it was forced to 1 above.
+ value.setDate(focusedDate.getDate());
+
+ selectDate(focusedDate);
+ } else {
+ VConsole.log("Trying to select a the focused date which is NULL!");
+ }
+ }
+
+ protected boolean onValueChange() {
+ return false;
+ }
+
+ public int getResolution() {
+ return resolution;
+ }
+
+ public void setResolution(int resolution) {
+ this.resolution = resolution;
+ if (time != null) {
+ time.removeFromParent();
+ time = null;
+ }
+ }
+
+ private boolean isReadonly() {
+ return readonly;
+ }
+
+ private boolean isEnabled() {
+ return enabled;
+ }
+
+ private void clearCalendarBody(boolean remove) {
+ if (!remove) {
+ // Leave the cells in place but clear their contents
+
+ // This has the side effect of ensuring that the calendar always
+ // contain 7 rows.
+ for (int row = 1; row < 7; row++) {
+ for (int col = 0; col < 8; col++) {
+ days.setHTML(row, col, " ");
+ }
+ }
+ } else if (getRowCount() > 1) {
+ removeRow(1);
+ days.clear();
+ }
+ }
+
+ /**
+ * Builds the top buttons and current month and year header.
+ *
+ * @param needsMonth
+ * Should the month buttons be visible?
+ */
+ private void buildCalendarHeader(boolean needsMonth) {
+
+ getRowFormatter().addStyleName(0,
+ VDateField.CLASSNAME + "-calendarpanel-header");
+
+ if (prevMonth == null && needsMonth) {
+ prevMonth = new VEventButton();
+ prevMonth.setHTML("‹");
+ prevMonth.setStyleName("v-button-prevmonth");
+ prevMonth.setTabIndex(-1);
+ nextMonth = new VEventButton();
+ nextMonth.setHTML("›");
+ nextMonth.setStyleName("v-button-nextmonth");
+ nextMonth.setTabIndex(-1);
+ getFlexCellFormatter().setStyleName(0, 3,
+ VDateField.CLASSNAME + "-calendarpanel-nextmonth");
+ getFlexCellFormatter().setStyleName(0, 1,
+ VDateField.CLASSNAME + "-calendarpanel-prevmonth");
+
+ setWidget(0, 3, nextMonth);
+ setWidget(0, 1, prevMonth);
+ } else if (prevMonth != null && !needsMonth) {
+ // Remove month traverse buttons
+ remove(prevMonth);
+ remove(nextMonth);
+ prevMonth = null;
+ nextMonth = null;
+ }
+
+ if (prevYear == null) {
+ prevYear = new VEventButton();
+ prevYear.setHTML("«");
+ prevYear.setStyleName("v-button-prevyear");
+ prevYear.setTabIndex(-1);
+ nextYear = new VEventButton();
+ nextYear.setHTML("»");
+ nextYear.setStyleName("v-button-nextyear");
+ nextYear.setTabIndex(-1);
+ setWidget(0, 0, prevYear);
+ setWidget(0, 4, nextYear);
+ getFlexCellFormatter().setStyleName(0, 0,
+ VDateField.CLASSNAME + "-calendarpanel-prevyear");
+ getFlexCellFormatter().setStyleName(0, 4,
+ VDateField.CLASSNAME + "-calendarpanel-nextyear");
+ }
+
+ final String monthName = needsMonth ? getDateTimeService().getMonth(
- if (focusedDate.getMonth() == oldMonth) {
++ displayedMonth.getMonth()) : "";
++ final int year = displayedMonth.getYear() + 1900;
+ getFlexCellFormatter().setStyleName(0, 2,
+ VDateField.CLASSNAME + "-calendarpanel-month");
+ setHTML(0, 2, "<span class=\"" + VDateField.CLASSNAME
+ + "-calendarpanel-month\">" + monthName + " " + year
+ + "</span>");
+ }
+
+ private DateTimeService getDateTimeService() {
+ return dateTimeService;
+ }
+
+ public void setDateTimeService(DateTimeService dateTimeService) {
+ this.dateTimeService = dateTimeService;
+ }
+
+ /**
+ * Returns whether ISO 8601 week numbers should be shown in the value
+ * selector or not. ISO 8601 defines that a week always starts with a Monday
+ * so the week numbers are only shown if this is the case.
+ *
+ * @return true if week number should be shown, false otherwise
+ */
+ public boolean isShowISOWeekNumbers() {
+ return showISOWeekNumbers;
+ }
+
+ public void setShowISOWeekNumbers(boolean showISOWeekNumbers) {
+ this.showISOWeekNumbers = showISOWeekNumbers;
+ }
+
+ /**
+ * Builds the day and time selectors of the calendar.
+ */
+ private void buildCalendarBody() {
+
+ final int weekColumn = 0;
+ final int firstWeekdayColumn = 1;
+ final int headerRow = 0;
+
+ setWidget(1, 0, days);
+ setCellPadding(0);
+ setCellSpacing(0);
+ getFlexCellFormatter().setColSpan(1, 0, 5);
+ getFlexCellFormatter().setStyleName(1, 0,
+ VDateField.CLASSNAME + "-calendarpanel-body");
+
+ days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
+ "v-week");
+ days.setHTML(headerRow, weekColumn, "<strong></strong>");
+ // Hide the week column if week numbers are not to be displayed.
+ days.getFlexCellFormatter().setVisible(headerRow, weekColumn,
+ isShowISOWeekNumbers());
+
+ days.getRowFormatter().setStyleName(headerRow,
+ VDateField.CLASSNAME + "-calendarpanel-weekdays");
+
+ if (isShowISOWeekNumbers()) {
+ days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
+ "v-first");
+ days.getFlexCellFormatter().setStyleName(headerRow,
+ firstWeekdayColumn, "");
+ days.getRowFormatter().addStyleName(headerRow,
+ VDateField.CLASSNAME + "-calendarpanel-weeknumbers");
+ } else {
+ days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, "");
+ days.getFlexCellFormatter().setStyleName(headerRow,
+ firstWeekdayColumn, "v-first");
+ }
+
+ days.getFlexCellFormatter().setStyleName(headerRow,
+ firstWeekdayColumn + 6, "v-last");
+
+ // Print weekday names
+ final int firstDay = getDateTimeService().getFirstDayOfWeek();
+ for (int i = 0; i < 7; i++) {
+ int day = i + firstDay;
+ if (day > 6) {
+ day = 0;
+ }
+ if (getResolution() > VDateField.RESOLUTION_MONTH) {
+ days.setHTML(headerRow, firstWeekdayColumn + i, "<strong>"
+ + getDateTimeService().getShortDay(day) + "</strong>");
+ } else {
+ days.setHTML(headerRow, firstWeekdayColumn + i, "");
+ }
+ }
+
+ // Zero out hours, minutes, seconds, and milliseconds to compare dates
+ // without time part
+ final Date tmp = new Date();
+ final Date today = new Date(tmp.getYear(), tmp.getMonth(),
+ tmp.getDate());
+
+ final Date selectedDate = value == null ? null : new Date(
+ value.getYear(), value.getMonth(), value.getDate());
+
+ final int startWeekDay = getDateTimeService().getStartWeekDay(
+ displayedMonth);
+ final Date curr = (Date) displayedMonth.clone();
+ // Start from the first day of the week that at least partially belongs
+ // to the current month
+ curr.setDate(1 - startWeekDay);
+
+ // No month has more than 6 weeks so 6 is a safe maximum for rows.
+ for (int weekOfMonth = 1; weekOfMonth < 7; weekOfMonth++) {
+ for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
+
+ // Actually write the day of month
+ Day day = new Day((Date) curr.clone());
+
+ if (curr.equals(selectedDate)) {
+ day.addStyleDependentName(CN_SELECTED);
+ selectedDay = day;
+ }
+ if (curr.equals(today)) {
+ day.addStyleDependentName(CN_TODAY);
+ }
+ if (curr.equals(focusedDate)) {
+ focusedDay = day;
+ focusedRow = weekOfMonth;
+ if (hasFocus) {
+ day.addStyleDependentName(CN_FOCUSED);
+ }
+ }
+ if (curr.getMonth() != displayedMonth.getMonth()) {
+ day.addStyleDependentName(CN_OFFMONTH);
+ }
+
+ days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek, day);
+
+ // ISO week numbers if requested
+ days.getCellFormatter().setVisible(weekOfMonth, weekColumn,
+ isShowISOWeekNumbers());
+ if (isShowISOWeekNumbers()) {
+ final String baseCssClass = VDateField.CLASSNAME
+ + "-calendarpanel-weeknumber";
+ String weekCssClass = baseCssClass;
+
+ int weekNumber = DateTimeService.getISOWeekNumber(curr);
+
+ days.setHTML(weekOfMonth, 0, "<span class=\""
+ + weekCssClass + "\"" + ">" + weekNumber
+ + "</span>");
+ }
+ curr.setDate(curr.getDate() + 1);
+ }
+ }
+ }
+
+ /**
+ * Do we need the time selector
+ *
+ * @return True if it is required
+ */
+ private boolean isTimeSelectorNeeded() {
+ return getResolution() > VDateField.RESOLUTION_DAY;
+ }
+
+ /**
+ * Updates the calendar and text field with the selected dates.
+ */
+ public void renderCalendar() {
+ if (focusedDate == null) {
+ Date now = new Date();
+ // focusedDate must have zero hours, mins, secs, millisecs
+ focusedDate = new Date(now.getYear(), now.getMonth(), now.getDate());
+ displayedMonth = new Date(now.getYear(), now.getMonth(), 1);
+ }
+
+ if (getResolution() <= VDateField.RESOLUTION_MONTH
+ && focusChangeListener != null) {
+ focusChangeListener.focusChanged(new Date(focusedDate.getTime()));
+ }
+
+ final boolean needsMonth = getResolution() > VDateField.RESOLUTION_YEAR;
+ boolean needsBody = getResolution() >= VDateField.RESOLUTION_DAY;
+ buildCalendarHeader(needsMonth);
+ clearCalendarBody(!needsBody);
+ if (needsBody) {
+ buildCalendarBody();
+ }
+
+ if (isTimeSelectorNeeded() && time == null) {
+ time = new VTime();
+ setWidget(2, 0, time);
+ getFlexCellFormatter().setColSpan(2, 0, 5);
+ getFlexCellFormatter().setStyleName(2, 0,
+ VDateField.CLASSNAME + "-calendarpanel-time");
+ } else if (isTimeSelectorNeeded()) {
+ time.updateTimes();
+ } else if (time != null) {
+ remove(time);
+ }
+ }
+
+ /**
+ * Moves the focus forward the given number of days.
+ */
+ private void focusNextDay(int days) {
+ int oldMonth = focusedDate.getMonth();
++ int oldYear = focusedDate.getYear();
+ focusedDate.setDate(focusedDate.getDate() + days);
+
++ if (focusedDate.getMonth() == oldMonth
++ && focusedDate.getYear() == oldYear) {
+ // Month did not change, only move the selection
+ focusDay(focusedDate);
+ } else {
+ // If the month changed we need to re-render the calendar
+ displayedMonth.setMonth(focusedDate.getMonth());
++ displayedMonth.setYear(focusedDate.getYear());
+ renderCalendar();
+ }
+ }
+
+ /**
+ * Moves the focus backward the given number of days.
+ */
+ private void focusPreviousDay(int days) {
+ focusNextDay(-days);
+ }
+
+ /**
+ * Selects the next month
+ */
+ private void focusNextMonth() {
+
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setMonth(currentMonth + 1);
+ int requestedMonth = (currentMonth + 1) % 12;
+
+ /*
+ * If the selected value was e.g. 31.3 the new value would be 31.4 but
+ * this value is invalid so the new value will be 1.5. This is taken
+ * care of by decreasing the value until we have the correct month.
+ */
+ while (focusedDate.getMonth() != requestedMonth) {
+ focusedDate.setDate(focusedDate.getDate() - 1);
+ }
+ displayedMonth.setMonth(displayedMonth.getMonth() + 1);
+
+ renderCalendar();
+ }
+
+ /**
+ * Selects the previous month
+ */
+ private void focusPreviousMonth() {
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setMonth(currentMonth - 1);
+
+ /*
+ * If the selected value was e.g. 31.12 the new value would be 31.11 but
+ * this value is invalid so the new value will be 1.12. This is taken
+ * care of by decreasing the value until we have the correct month.
+ */
+ while (focusedDate.getMonth() == currentMonth) {
+ focusedDate.setDate(focusedDate.getDate() - 1);
+ }
+ displayedMonth.setMonth(displayedMonth.getMonth() - 1);
+
+ renderCalendar();
+ }
+
+ /**
+ * Selects the previous year
+ */
+ private void focusPreviousYear(int years) {
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setYear(focusedDate.getYear() - years);
+ displayedMonth.setYear(displayedMonth.getYear() - years);
+ /*
+ * If the focused date was a leap day (Feb 29), the new date becomes Mar
+ * 1 if the new year is not also a leap year. Set it to Feb 28 instead.
+ */
+ if (focusedDate.getMonth() != currentMonth) {
+ focusedDate.setDate(0);
+ }
+ renderCalendar();
+ }
+
+ /**
+ * Selects the next year
+ */
+ private void focusNextYear(int years) {
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setYear(focusedDate.getYear() + years);
+ displayedMonth.setYear(displayedMonth.getYear() + years);
+ /*
+ * If the focused date was a leap day (Feb 29), the new date becomes Mar
+ * 1 if the new year is not also a leap year. Set it to Feb 28 instead.
+ */
+ if (focusedDate.getMonth() != currentMonth) {
+ focusedDate.setDate(0);
+ }
+ renderCalendar();
+ }
+
+ /**
+ * Handles a user click on the component
+ *
+ * @param sender
+ * The component that was clicked
+ * @param updateVariable
+ * Should the value field be updated
+ *
+ */
+ private void processClickEvent(Widget sender) {
+ if (!isEnabled() || isReadonly()) {
+ return;
+ }
+ if (sender == prevYear) {
+ focusPreviousYear(1);
+ } else if (sender == nextYear) {
+ focusNextYear(1);
+ } else if (sender == prevMonth) {
+ focusPreviousMonth();
+ } else if (sender == nextMonth) {
+ focusNextMonth();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ handleKeyPress(event);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
+ * .gwt.event.dom.client.KeyPressEvent)
+ */
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ handleKeyPress(event);
+ }
+
+ /**
+ * Handles the keypress from both the onKeyPress event and the onKeyDown
+ * event
+ *
+ * @param event
+ * The keydown/keypress event
+ */
+ private void handleKeyPress(DomEvent<?> event) {
+ if (time != null
+ && time.getElement().isOrHasChild(
+ (Node) event.getNativeEvent().getEventTarget().cast())) {
+ int nativeKeyCode = event.getNativeEvent().getKeyCode();
+ if (nativeKeyCode == getSelectKey()) {
+ onSubmit(); // submit happens if enter key hit down on listboxes
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ return;
+ }
+
+ // Check tabs
+ int keycode = event.getNativeEvent().getKeyCode();
+ if (keycode == KeyCodes.KEY_TAB && event.getNativeEvent().getShiftKey()) {
+ if (onTabOut(event)) {
+ return;
+ }
+ }
+
+ // Handle the navigation
+ if (handleNavigation(keycode, event.getNativeEvent().getCtrlKey()
+ || event.getNativeEvent().getMetaKey(), event.getNativeEvent()
+ .getShiftKey())) {
+ event.preventDefault();
+ }
+
+ }
+
+ /**
+ * Notifies submit-listeners of a submit event
+ */
+ private void onSubmit() {
+ if (getSubmitListener() != null) {
+ getSubmitListener().onSubmit();
+ }
+ }
+
+ /**
+ * Notifies submit-listeners of a cancel event
+ */
+ private void onCancel() {
+ if (getSubmitListener() != null) {
+ getSubmitListener().onCancel();
+ }
+ }
+
+ /**
+ * Handles the keyboard navigation when the resolution is set to years.
+ *
+ * @param keycode
+ * The keycode to process
+ * @param ctrl
+ * Is ctrl pressed?
+ * @param shift
+ * is shift pressed
+ * @return Returns true if the keycode was processed, else false
+ */
+ protected boolean handleNavigationYearMode(int keycode, boolean ctrl,
+ boolean shift) {
+
+ // Ctrl and Shift selection not supported
+ if (ctrl || shift) {
+ return false;
+ }
+
+ else if (keycode == getPreviousKey()) {
+ focusNextYear(10); // Add 10 years
+ return true;
+ }
+
+ else if (keycode == getForwardKey()) {
+ focusNextYear(1); // Add 1 year
+ return true;
+ }
+
+ else if (keycode == getNextKey()) {
+ focusPreviousYear(10); // Subtract 10 years
+ return true;
+ }
+
+ else if (keycode == getBackwardKey()) {
+ focusPreviousYear(1); // Subtract 1 year
+ return true;
+
+ } else if (keycode == getSelectKey()) {
+ value = (Date) focusedDate.clone();
+ onSubmit();
+ return true;
+
+ } else if (keycode == getResetKey()) {
+ // Restore showing value the selected value
+ focusedDate.setTime(value.getTime());
+ renderCalendar();
+ return true;
+
+ } else if (keycode == getCloseKey()) {
+ // TODO fire listener, on users responsibility??
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle the keyboard navigation when the resolution is set to MONTH
+ *
+ * @param keycode
+ * The keycode to handle
+ * @param ctrl
+ * Was the ctrl key pressed?
+ * @param shift
+ * Was the shift key pressed?
+ * @return
+ */
+ protected boolean handleNavigationMonthMode(int keycode, boolean ctrl,
+ boolean shift) {
+
+ // Ctrl selection not supported
+ if (ctrl) {
+ return false;
+
+ } else if (keycode == getPreviousKey()) {
+ focusNextYear(1); // Add 1 year
+ return true;
+
+ } else if (keycode == getForwardKey()) {
+ focusNextMonth(); // Add 1 month
+ return true;
+
+ } else if (keycode == getNextKey()) {
+ focusPreviousYear(1); // Subtract 1 year
+ return true;
+
+ } else if (keycode == getBackwardKey()) {
+ focusPreviousMonth(); // Subtract 1 month
+ return true;
+
+ } else if (keycode == getSelectKey()) {
+ value = (Date) focusedDate.clone();
+ onSubmit();
+ return true;
+
+ } else if (keycode == getResetKey()) {
+ // Restore showing value the selected value
+ focusedDate.setTime(value.getTime());
+ renderCalendar();
+ return true;
+
+ } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) {
+
+ // TODO fire close event
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle keyboard navigation what the resolution is set to DAY
+ *
+ * @param keycode
+ * The keycode to handle
+ * @param ctrl
+ * Was the ctrl key pressed?
+ * @param shift
+ * Was the shift key pressed?
+ * @return Return true if the key press was handled by the method, else
+ * return false.
+ */
+ protected boolean handleNavigationDayMode(int keycode, boolean ctrl,
+ boolean shift) {
+
+ // Ctrl key is not in use
+ if (ctrl) {
+ return false;
+ }
+
+ /*
+ * Jumps to the next day.
+ */
+ if (keycode == getForwardKey() && !shift) {
+ focusNextDay(1);
+ return true;
+
+ /*
+ * Jumps to the previous day
+ */
+ } else if (keycode == getBackwardKey() && !shift) {
+ focusPreviousDay(1);
+ return true;
+
+ /*
+ * Jumps one week forward in the calendar
+ */
+ } else if (keycode == getNextKey() && !shift) {
+ focusNextDay(7);
+ return true;
+
+ /*
+ * Jumps one week back in the calendar
+ */
+ } else if (keycode == getPreviousKey() && !shift) {
+ focusPreviousDay(7);
+ return true;
+
+ /*
+ * Selects the value that is chosen
+ */
+ } else if (keycode == getSelectKey() && !shift) {
+ selectFocused();
+ onSubmit(); // submit
+ return true;
+
+ } else if (keycode == getCloseKey()) {
+ onCancel();
+ // TODO close event
+
+ return true;
+
+ /*
+ * Jumps to the next month
+ */
+ } else if (shift && keycode == getForwardKey()) {
+ focusNextMonth();
+ return true;
+
+ /*
+ * Jumps to the previous month
+ */
+ } else if (shift && keycode == getBackwardKey()) {
+ focusPreviousMonth();
+ return true;
+
+ /*
+ * Jumps to the next year
+ */
+ } else if (shift && keycode == getPreviousKey()) {
+ focusNextYear(1);
+ return true;
+
+ /*
+ * Jumps to the previous year
+ */
+ } else if (shift && keycode == getNextKey()) {
+ focusPreviousYear(1);
+ return true;
+
+ /*
+ * Resets the selection
+ */
+ } else if (keycode == getResetKey() && !shift) {
+ // Restore showing value the selected value
+ focusedDate = new Date(value.getYear(), value.getMonth(),
+ value.getDate());
+ displayedMonth = new Date(value.getYear(), value.getMonth(), 1);
+ renderCalendar();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles the keyboard navigation
+ *
+ * @param keycode
+ * The key code that was pressed
+ * @param ctrl
+ * Was the ctrl key pressed
+ * @param shift
+ * Was the shift key pressed
+ * @return Return true if key press was handled by the component, else
+ * return false
+ */
+ protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+ if (!isEnabled() || isReadonly()) {
+ return false;
+ }
+
+ else if (resolution == VDateField.RESOLUTION_YEAR) {
+ return handleNavigationYearMode(keycode, ctrl, shift);
+ }
+
+ else if (resolution == VDateField.RESOLUTION_MONTH) {
+ return handleNavigationMonthMode(keycode, ctrl, shift);
+ }
+
+ else if (resolution == VDateField.RESOLUTION_DAY) {
+ return handleNavigationDayMode(keycode, ctrl, shift);
+ }
+
+ else {
+ return handleNavigationDayMode(keycode, ctrl, shift);
+ }
+
+ }
+
+ /**
+ * Returns the reset key which will reset the calendar to the previous
+ * selection. By default this is backspace but it can be overriden to change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getResetKey() {
+ return KeyCodes.KEY_BACKSPACE;
+ }
+
+ /**
+ * Returns the select key which selects the value. By default this is the
+ * enter key but it can be changed to whatever you like by overriding this
+ * method.
+ *
+ * @return
+ */
+ protected int getSelectKey() {
+ return KeyCodes.KEY_ENTER;
+ }
+
+ /**
+ * Returns the key that closes the popup window if this is a VPopopCalendar.
+ * Else this does nothing. By default this is the Escape key but you can
+ * change the key to whatever you want by overriding this method.
+ *
+ * @return
+ */
+ protected int getCloseKey() {
+ return KeyCodes.KEY_ESCAPE;
+ }
+
+ /**
+ * The key that selects the next day in the calendar. By default this is the
+ * right arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getForwardKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * The key that selects the previous day in the calendar. By default this is
+ * the left arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getBackwardKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * The key that selects the next week in the calendar. By default this is
+ * the down arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getNextKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * The key that selects the previous week in the calendar. By default this
+ * is the up arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getPreviousKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google
+ * .gwt.event.dom.client.MouseOutEvent)
+ */
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ if (mouseTimer != null) {
+ mouseTimer.cancel();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google
+ * .gwt.event.dom.client.MouseDownEvent)
+ */
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ // Allow user to click-n-hold for fast-forward or fast-rewind.
+ // Timer is first used for a 500ms delay after mousedown. After that has
+ // elapsed, another timer is triggered to go off every 150ms. Both
+ // timers are cancelled on mouseup or mouseout.
+ if (event.getSource() instanceof VEventButton) {
+ final VEventButton sender = (VEventButton) event.getSource();
+ processClickEvent(sender);
+ mouseTimer = new Timer() {
+ @Override
+ public void run() {
+ mouseTimer = new Timer() {
+ @Override
+ public void run() {
+ processClickEvent(sender);
+ }
+ };
+ mouseTimer.scheduleRepeating(150);
+ }
+ };
+ mouseTimer.schedule(500);
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseUpHandler#onMouseUp(com.google.gwt
+ * .event.dom.client.MouseUpEvent)
+ */
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ if (mouseTimer != null) {
+ mouseTimer.cancel();
+ }
+ }
+
+ /**
+ * Sets the data of the Panel.
+ *
+ * @param currentDate
+ * The date to set
+ */
+ public void setDate(Date currentDate) {
+
+ // Check that we are not re-rendering an already active date
+ if (currentDate == value && currentDate != null) {
+ return;
+ }
+
+ Date oldDisplayedMonth = displayedMonth;
+ value = currentDate;
+
+ if (value == null) {
+ focusedDate = displayedMonth = null;
+ } else {
+ focusedDate = new Date(value.getYear(), value.getMonth(),
+ value.getDate());
+ displayedMonth = new Date(value.getYear(), value.getMonth(), 1);
+ }
+
+ // Re-render calendar if the displayed month is changed,
+ // or if a time selector is needed but does not exist.
+ if ((isTimeSelectorNeeded() && time == null)
+ || oldDisplayedMonth == null || value == null
+ || oldDisplayedMonth.getYear() != value.getYear()
+ || oldDisplayedMonth.getMonth() != value.getMonth()) {
+ renderCalendar();
+ } else {
+ focusDay(focusedDate);
+ selectFocused();
+ if (isTimeSelectorNeeded()) {
+ time.updateTimes();
+ }
+ }
+
+ if (!hasFocus) {
+ focusDay(null);
+ }
+ }
+
+ /**
+ * TimeSelector is a widget consisting of list boxes that modifie the Date
+ * object that is given for.
+ *
+ */
+ public class VTime extends FlowPanel implements ChangeHandler {
+
+ private ListBox hours;
+
+ private ListBox mins;
+
+ private ListBox sec;
+
+ private ListBox ampm;
+
+ /**
+ * Constructor
+ */
+ public VTime() {
+ super();
+ setStyleName(VDateField.CLASSNAME + "-time");
+ buildTime();
+ }
+
+ private ListBox createListBox() {
+ ListBox lb = new ListBox();
+ lb.setStyleName(VNativeSelect.CLASSNAME);
+ lb.addChangeHandler(this);
+ lb.addBlurHandler(VCalendarPanel.this);
+ lb.addFocusHandler(VCalendarPanel.this);
+ return lb;
+ }
+
+ /**
+ * Constructs the ListBoxes and updates their value
+ *
+ * @param redraw
+ * Should new instances of the listboxes be created
+ */
+ private void buildTime() {
+ clear();
+
+ hours = createListBox();
+ if (getDateTimeService().isTwelveHourClock()) {
+ hours.addItem("12");
+ for (int i = 1; i < 12; i++) {
+ hours.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ } else {
+ for (int i = 0; i < 24; i++) {
+ hours.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ }
+
+ hours.addChangeHandler(this);
+ if (getDateTimeService().isTwelveHourClock()) {
+ ampm = createListBox();
+ final String[] ampmText = getDateTimeService().getAmPmStrings();
+ ampm.addItem(ampmText[0]);
+ ampm.addItem(ampmText[1]);
+ ampm.addChangeHandler(this);
+ }
+
+ if (getResolution() >= VDateField.RESOLUTION_MIN) {
+ mins = createListBox();
+ for (int i = 0; i < 60; i++) {
+ mins.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ mins.addChangeHandler(this);
+ }
+ if (getResolution() >= VDateField.RESOLUTION_SEC) {
+ sec = createListBox();
+ for (int i = 0; i < 60; i++) {
+ sec.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ sec.addChangeHandler(this);
+ }
+
+ final String delimiter = getDateTimeService().getClockDelimeter();
+ if (isReadonly()) {
+ int h = 0;
+ if (value != null) {
+ h = value.getHours();
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ h -= h < 12 ? 0 : 12;
+ }
+ add(new VLabel(h < 10 ? "0" + h : "" + h));
+ } else {
+ add(hours);
+ }
+
+ if (getResolution() >= VDateField.RESOLUTION_MIN) {
+ add(new VLabel(delimiter));
+ if (isReadonly()) {
+ final int m = mins.getSelectedIndex();
+ add(new VLabel(m < 10 ? "0" + m : "" + m));
+ } else {
+ add(mins);
+ }
+ }
+ if (getResolution() >= VDateField.RESOLUTION_SEC) {
+ add(new VLabel(delimiter));
+ if (isReadonly()) {
+ final int s = sec.getSelectedIndex();
+ add(new VLabel(s < 10 ? "0" + s : "" + s));
+ } else {
+ add(sec);
+ }
+ }
+ if (getResolution() == VDateField.RESOLUTION_HOUR) {
+ add(new VLabel(delimiter + "00")); // o'clock
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ add(new VLabel(" "));
+ if (isReadonly()) {
+ int i = 0;
+ if (value != null) {
+ i = (value.getHours() < 12) ? 0 : 1;
+ }
+ add(new VLabel(ampm.getItemText(i)));
+ } else {
+ add(ampm);
+ }
+ }
+
+ if (isReadonly()) {
+ return;
+ }
+
+ // Update times
+ updateTimes();
+
+ ListBox lastDropDown = getLastDropDown();
+ lastDropDown.addKeyDownHandler(new KeyDownHandler() {
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ boolean shiftKey = event.getNativeEvent().getShiftKey();
+ if (shiftKey) {
+ return;
+ } else {
+ int nativeKeyCode = event.getNativeKeyCode();
+ if (nativeKeyCode == KeyCodes.KEY_TAB) {
+ onTabOut(event);
+ }
+ }
+ }
+ });
+
+ }
+
+ private ListBox getLastDropDown() {
+ int i = getWidgetCount() - 1;
+ while (i >= 0) {
+ Widget widget = getWidget(i);
+ if (widget instanceof ListBox) {
+ return (ListBox) widget;
+ }
+ i--;
+ }
+ return null;
+ }
+
+ /**
+ * Updates the valus to correspond to the values in value
+ */
+ public void updateTimes() {
+ boolean selected = true;
+ if (value == null) {
+ value = new Date();
+ selected = false;
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ int h = value.getHours();
+ ampm.setSelectedIndex(h < 12 ? 0 : 1);
+ h -= ampm.getSelectedIndex() * 12;
+ hours.setSelectedIndex(h);
+ } else {
+ hours.setSelectedIndex(value.getHours());
+ }
+ if (getResolution() >= VDateField.RESOLUTION_MIN) {
+ mins.setSelectedIndex(value.getMinutes());
+ }
+ if (getResolution() >= VDateField.RESOLUTION_SEC) {
+ sec.setSelectedIndex(value.getSeconds());
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ ampm.setSelectedIndex(value.getHours() < 12 ? 0 : 1);
+ }
+
+ hours.setEnabled(isEnabled());
+ if (mins != null) {
+ mins.setEnabled(isEnabled());
+ }
+ if (sec != null) {
+ sec.setEnabled(isEnabled());
+ }
+ if (ampm != null) {
+ ampm.setEnabled(isEnabled());
+ }
+
+ }
+
+ private int getMilliseconds() {
+ return DateTimeService.getMilliseconds(value);
+ }
+
+ private DateTimeService getDateTimeService() {
+ if (dateTimeService == null) {
+ dateTimeService = new DateTimeService();
+ }
+ return dateTimeService;
+ }
+
+ /*
+ * (non-Javadoc) VT
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ChangeHandler#onChange(com.google.gwt
+ * .event.dom.client.ChangeEvent)
+ */
+ @Override
+ public void onChange(ChangeEvent event) {
+ /*
+ * Value from dropdowns gets always set for the value. Like year and
+ * month when resolution is month or year.
+ */
+ if (event.getSource() == hours) {
+ int h = hours.getSelectedIndex();
+ if (getDateTimeService().isTwelveHourClock()) {
+ h = h + ampm.getSelectedIndex() * 12;
+ }
+ value.setHours(h);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(h, value.getMinutes(),
+ value.getSeconds(),
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getSource() == mins) {
+ final int m = mins.getSelectedIndex();
+ value.setMinutes(m);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(value.getHours(), m,
+ value.getSeconds(),
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getSource() == sec) {
+ final int s = sec.getSelectedIndex();
+ value.setSeconds(s);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(value.getHours(),
+ value.getMinutes(), s,
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getSource() == ampm) {
+ final int h = hours.getSelectedIndex()
+ + (ampm.getSelectedIndex() * 12);
+ value.setHours(h);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(h, value.getMinutes(),
+ value.getSeconds(),
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ }
+
+ /**
+ * A widget representing a single day in the calendar panel.
+ */
+ private class Day extends InlineHTML {
+ private static final String BASECLASS = VDateField.CLASSNAME
+ + "-calendarpanel-day";
+ private final Date date;
+
+ Day(Date date) {
+ super("" + date.getDate());
+ setStyleName(BASECLASS);
+ this.date = date;
+ addClickHandler(dayClickHandler);
+ }
+
+ public Date getDate() {
+ return date;
+ }
+ }
+
+ public Date getDate() {
+ return value;
+ }
+
+ /**
+ * If true should be returned if the panel will not be used after this
+ * event.
+ *
+ * @param event
+ * @return
+ */
+ protected boolean onTabOut(DomEvent<?> event) {
+ if (focusOutListener != null) {
+ return focusOutListener.onFocusOut(event);
+ }
+ return false;
+ }
+
+ /**
+ * A focus out listener is triggered when the panel loosed focus. This can
+ * happen either after a user clicks outside the panel or tabs out.
+ *
+ * @param listener
+ * The listener to trigger
+ */
+ public void setFocusOutListener(FocusOutListener listener) {
+ focusOutListener = listener;
+ }
+
+ /**
+ * The submit listener is called when the user selects a value from the
+ * calender either by clicking the day or selects it by keyboard.
+ *
+ * @param submitListener
+ * The listener to trigger
+ */
+ public void setSubmitListener(SubmitListener submitListener) {
+ this.submitListener = submitListener;
+ }
+
+ /**
+ * The given FocusChangeListener is notified when the focused date changes
+ * by user either clicking on a new date or by using the keyboard.
+ *
+ * @param listener
+ * The FocusChangeListener to be notified
+ */
+ public void setFocusChangeListener(FocusChangeListener listener) {
+ focusChangeListener = listener;
+ }
+
+ /**
+ * The time change listener is triggered when the user changes the time.
+ *
+ * @param listener
+ */
+ public void setTimeChangeListener(TimeChangeListener listener) {
+ timeChangeListener = listener;
+ }
+
+ /**
+ * Returns the submit listener that listens to selection made from the panel
+ *
+ * @return The listener or NULL if no listener has been set
+ */
+ public SubmitListener getSubmitListener() {
+ return submitListener;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+ @Override
+ public void onBlur(final BlurEvent event) {
+ if (event.getSource() instanceof VCalendarPanel) {
+ hasFocus = false;
+ focusDay(null);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ @Override
+ public void onFocus(FocusEvent event) {
+ if (event.getSource() instanceof VCalendarPanel) {
+ hasFocus = true;
+
+ // Focuses the current day if the calendar shows the days
+ if (focusedDay != null) {
+ focusDay(focusedDate);
+ }
+ }
+ }
+
+ private static final String SUBPART_NEXT_MONTH = "nextmon";
+ private static final String SUBPART_PREV_MONTH = "prevmon";
+
+ private static final String SUBPART_NEXT_YEAR = "nexty";
+ private static final String SUBPART_PREV_YEAR = "prevy";
+ private static final String SUBPART_HOUR_SELECT = "h";
+ private static final String SUBPART_MINUTE_SELECT = "m";
+ private static final String SUBPART_SECS_SELECT = "s";
+ private static final String SUBPART_MSECS_SELECT = "ms";
+ private static final String SUBPART_AMPM_SELECT = "ampm";
+ private static final String SUBPART_DAY = "day";
+ private static final String SUBPART_MONTH_YEAR_HEADER = "header";
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (contains(nextMonth, subElement)) {
+ return SUBPART_NEXT_MONTH;
+ } else if (contains(prevMonth, subElement)) {
+ return SUBPART_PREV_MONTH;
+ } else if (contains(nextYear, subElement)) {
+ return SUBPART_NEXT_YEAR;
+ } else if (contains(prevYear, subElement)) {
+ return SUBPART_PREV_YEAR;
+ } else if (contains(days, subElement)) {
+ // Day, find out which dayOfMonth and use that as the identifier
+ Day day = Util.findWidget(subElement, Day.class);
+ if (day != null) {
+ Date date = day.getDate();
+ int id = date.getDate();
+ // Zero or negative ids map to days of the preceding month,
+ // past-the-end-of-month ids to days of the following month
+ if (date.getMonth() < displayedMonth.getMonth()) {
+ id -= DateTimeService.getNumberOfDaysInMonth(date);
+ } else if (date.getMonth() > displayedMonth.getMonth()) {
+ id += DateTimeService
+ .getNumberOfDaysInMonth(displayedMonth);
+ }
+ return SUBPART_DAY + id;
+ }
+ } else if (time != null) {
+ if (contains(time.hours, subElement)) {
+ return SUBPART_HOUR_SELECT;
+ } else if (contains(time.mins, subElement)) {
+ return SUBPART_MINUTE_SELECT;
+ } else if (contains(time.sec, subElement)) {
+ return SUBPART_SECS_SELECT;
+ } else if (contains(time.ampm, subElement)) {
+ return SUBPART_AMPM_SELECT;
+
+ }
+ } else if (getCellFormatter().getElement(0, 2).isOrHasChild(subElement)) {
+ return SUBPART_MONTH_YEAR_HEADER;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if subElement is inside the widget DOM hierarchy.
+ *
+ * @param w
+ * @param subElement
+ * @return true if {@code w} is a parent of subElement, false otherwise.
+ */
+ private boolean contains(Widget w, Element subElement) {
+ if (w == null || w.getElement() == null) {
+ return false;
+ }
+
+ return w.getElement().isOrHasChild(subElement);
+ }
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (SUBPART_NEXT_MONTH.equals(subPart)) {
+ return nextMonth.getElement();
+ }
+ if (SUBPART_PREV_MONTH.equals(subPart)) {
+ return prevMonth.getElement();
+ }
+ if (SUBPART_NEXT_YEAR.equals(subPart)) {
+ return nextYear.getElement();
+ }
+ if (SUBPART_PREV_YEAR.equals(subPart)) {
+ return prevYear.getElement();
+ }
+ if (SUBPART_HOUR_SELECT.equals(subPart)) {
+ return time.hours.getElement();
+ }
+ if (SUBPART_MINUTE_SELECT.equals(subPart)) {
+ return time.mins.getElement();
+ }
+ if (SUBPART_SECS_SELECT.equals(subPart)) {
+ return time.sec.getElement();
+ }
+ if (SUBPART_AMPM_SELECT.equals(subPart)) {
+ return time.ampm.getElement();
+ }
+ if (subPart.startsWith(SUBPART_DAY)) {
+ // Zero or negative ids map to days in the preceding month,
+ // past-the-end-of-month ids to days in the following month
+ int dayOfMonth = Integer.parseInt(subPart.substring(SUBPART_DAY
+ .length()));
+ Date date = new Date(displayedMonth.getYear(),
+ displayedMonth.getMonth(), dayOfMonth);
+ Iterator<Widget> iter = days.iterator();
+ while (iter.hasNext()) {
+ Widget w = iter.next();
+ if (w instanceof Day) {
+ Day day = (Day) w;
+ if (day.getDate().equals(date)) {
+ return day.getElement();
+ }
+ }
+ }
+ }
+
+ if (SUBPART_MONTH_YEAR_HEADER.equals(subPart)) {
+ return (Element) getCellFormatter().getElement(0, 2).getChild(0);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (mouseTimer != null) {
+ mouseTimer.cancel();
+ }
+ }
+}
--- /dev/null
- if (dataSource != null) {
-
- // Gets the correct value from datasource
- T newFieldValue;
- try {
-
- // Discards buffer by overwriting from datasource
- newFieldValue = convertFromDataSource(getDataSourceValue());
-
- // If successful, remove set the buffering state to be ok
- if (getCurrentBufferedSourceException() != null) {
- setCurrentBufferedSourceException(null);
- }
- } catch (final Throwable e) {
- // FIXME: What should really be done here if conversion fails?
-
- // Sets the buffering state
- currentBufferedSourceException = new Buffered.SourceException(
- this, e);
- markAsDirty();
-
- // Throws the source exception
- throw currentBufferedSourceException;
- }
-
- final boolean wasModified = isModified();
- setModified(false);
-
- // If the new value differs from the previous one
- if (!equals(newFieldValue, getInternalValue())) {
- setInternalValue(newFieldValue);
- fireValueChange(false);
- } else if (wasModified) {
- // If the value did not change, but the modification status did
- markAsDirty();
- }
- }
+/*
+ * Copyright 2011 Vaadin Ltd.
+ *
+ * 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 com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Buffered;
+import com.vaadin.data.Property;
+import com.vaadin.data.Validatable;
+import com.vaadin.data.Validator;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.Converter.ConversionException;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.Action;
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.event.ShortcutListener;
+import com.vaadin.server.AbstractErrorMessage;
+import com.vaadin.server.CompositeErrorMessage;
+import com.vaadin.server.ErrorMessage;
+import com.vaadin.shared.AbstractFieldState;
+
+/**
+ * <p>
+ * Abstract field component for implementing buffered property editors. The
+ * field may hold an internal value, or it may be connected to any data source
+ * that implements the {@link com.vaadin.data.Property}interface.
+ * <code>AbstractField</code> implements that interface itself, too, so
+ * accessing the Property value represented by it is straightforward.
+ * </p>
+ *
+ * <p>
+ * AbstractField also provides the {@link com.vaadin.data.Buffered} interface
+ * for buffering the data source value. By default the Field is in write
+ * through-mode and {@link #setWriteThrough(boolean)}should be called to enable
+ * buffering.
+ * </p>
+ *
+ * <p>
+ * The class also supports {@link com.vaadin.data.Validator validators} to make
+ * sure the value contained in the field is valid.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractField<T> extends AbstractComponent implements
+ Field<T>, Property.ReadOnlyStatusChangeListener,
+ Property.ReadOnlyStatusChangeNotifier, Action.ShortcutNotifier {
+
+ /* Private members */
+
+ private static final Logger logger = Logger.getLogger(AbstractField.class
+ .getName());
+
+ /**
+ * Value of the abstract field.
+ */
+ private T value;
+
+ /**
+ * A converter used to convert from the data model type to the field type
+ * and vice versa.
+ */
+ private Converter<T, Object> converter = null;
+ /**
+ * Connected data-source.
+ */
+ private Property<?> dataSource = null;
+
+ /**
+ * The list of validators.
+ */
+ private LinkedList<Validator> validators = null;
+
+ /**
+ * True if field is in buffered mode, false otherwise
+ */
+ private boolean buffered;
+
+ /**
+ * Flag to indicate that the field is currently committing its value to the
+ * datasource.
+ */
+ private boolean committingValueToDataSource = false;
+
+ /**
+ * Current source exception.
+ */
+ private Buffered.SourceException currentBufferedSourceException = null;
+
+ /**
+ * Are the invalid values allowed in fields ?
+ */
+ private boolean invalidAllowed = true;
+
+ /**
+ * Are the invalid values committed ?
+ */
+ private boolean invalidCommitted = false;
+
+ /**
+ * The error message for the exception that is thrown when the field is
+ * required but empty.
+ */
+ private String requiredError = "";
+
+ /**
+ * The error message that is shown when the field value cannot be converted.
+ */
+ private String conversionError = "Could not convert value to {0}";
+
+ /**
+ * Is automatic validation enabled.
+ */
+ private boolean validationVisible = true;
+
+ private boolean valueWasModifiedByDataSourceDuringCommit;
+
+ /**
+ * Whether this field is currently registered as listening to events from
+ * its data source.
+ *
+ * @see #setPropertyDataSource(Property)
+ * @see #addPropertyListeners()
+ * @see #removePropertyListeners()
+ */
+ private boolean isListeningToPropertyEvents = false;
+
+ /* Component basics */
+
+ /*
+ * Paints the field. Don't add a JavaDoc comment here, we use the default
+ * documentation from the implemented interface.
+ */
+
+ /**
+ * Returns true if the error indicator be hidden when painting the component
+ * even when there are errors.
+ *
+ * This is a mostly internal method, but can be overridden in subclasses
+ * e.g. if the error indicator should also be shown for empty fields in some
+ * cases.
+ *
+ * @return true to hide the error indicator, false to use the normal logic
+ * to show it when there are errors
+ */
+ protected boolean shouldHideErrors() {
+ // getErrorMessage() can still return something else than null based on
+ // validation etc.
+ return isRequired() && isEmpty() && getComponentError() == null;
+ }
+
+ /**
+ * Returns the type of the Field. The methods <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return the type of the Field
+ */
+ @Override
+ public abstract Class<? extends T> getType();
+
+ /**
+ * The abstract field is read only also if the data source is in read only
+ * mode.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly()
+ || (dataSource != null && dataSource.isReadOnly());
+ }
+
+ /**
+ * Changes the readonly state and throw read-only status change events.
+ *
+ * @see com.vaadin.ui.Component#setReadOnly(boolean)
+ */
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ super.setReadOnly(readOnly);
+ fireReadOnlyStatusChange();
+ }
+
+ /**
+ * Tests if the invalid data is committed to datasource.
+ *
+ * @see com.vaadin.data.BufferedValidatable#isInvalidCommitted()
+ */
+ @Override
+ public boolean isInvalidCommitted() {
+ return invalidCommitted;
+ }
+
+ /**
+ * Sets if the invalid data should be committed to datasource.
+ *
+ * @see com.vaadin.data.BufferedValidatable#setInvalidCommitted(boolean)
+ */
+ @Override
+ public void setInvalidCommitted(boolean isCommitted) {
+ invalidCommitted = isCommitted;
+ }
+
+ /*
+ * Saves the current value to the data source Don't add a JavaDoc comment
+ * here, we use the default documentation from the implemented interface.
+ */
+ @Override
+ public void commit() throws Buffered.SourceException, InvalidValueException {
+ if (dataSource != null && !dataSource.isReadOnly()) {
+ if ((isInvalidCommitted() || isValid())) {
+ try {
+
+ // Commits the value to datasource.
+ valueWasModifiedByDataSourceDuringCommit = false;
+ committingValueToDataSource = true;
+ getPropertyDataSource().setValue(getConvertedValue());
+ } catch (final Throwable e) {
+
+ // Sets the buffering state.
+ SourceException sourceException = new Buffered.SourceException(
+ this, e);
+ setCurrentBufferedSourceException(sourceException);
+
+ // Throws the source exception.
+ throw sourceException;
+ } finally {
+ committingValueToDataSource = false;
+ }
+ } else {
+ /* An invalid value and we don't allow them, throw the exception */
+ validate();
+ }
+ }
+
+ // The abstract field is not modified anymore
+ if (isModified()) {
+ setModified(false);
+ }
+
+ // If successful, remove set the buffering state to be ok
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+
+ if (valueWasModifiedByDataSourceDuringCommit) {
+ valueWasModifiedByDataSourceDuringCommit = false;
+ fireValueChange(false);
+ }
+
+ }
+
+ /*
+ * Updates the value from the data source. Don't add a JavaDoc comment here,
+ * we use the default documentation from the implemented interface.
+ */
+ @Override
+ public void discard() throws Buffered.SourceException {
- discard();
++ updateValueFromDataSource();
+ }
+
+ /**
+ * Gets the value from the data source. This is only here because of clarity
+ * in the code that handles both the data model value and the field value.
+ *
+ * @return The value of the property data source
+ */
+ private Object getDataSourceValue() {
+ return dataSource.getValue();
+ }
+
+ /**
+ * Returns the field value. This is always identical to {@link #getValue()}
+ * and only here because of clarity in the code that handles both the data
+ * model value and the field value.
+ *
+ * @return The value of the field
+ */
+ private T getFieldValue() {
+ // Give the value from abstract buffers if the field if possible
+ if (dataSource == null || isBuffered() || isModified()) {
+ return getInternalValue();
+ }
+
+ // There is no buffered value so use whatever the data model provides
+ return convertFromDataSource(getDataSourceValue());
+ }
+
+ /*
+ * Has the field been modified since the last commit()? Don't add a JavaDoc
+ * comment here, we use the default documentation from the implemented
+ * interface.
+ */
+ @Override
+ public boolean isModified() {
+ return getState().modified;
+ }
+
+ private void setModified(boolean modified) {
+ getState().modified = modified;
+ }
+
+ /**
+ * Sets the buffered mode of this Field.
+ * <p>
+ * When the field is in buffered mode, changes will not be committed to the
+ * property data source until {@link #commit()} is called.
+ * </p>
+ * <p>
+ * Setting buffered mode from true to false will commit any pending changes.
+ * </p>
+ * <p>
+ *
+ * </p>
+ *
+ * @since 7.0.0
+ * @param buffered
+ * true if buffered mode should be turned on, false otherwise
+ */
+ @Override
+ public void setBuffered(boolean buffered) {
+ if (this.buffered == buffered) {
+ return;
+ }
+ this.buffered = buffered;
+ if (!buffered) {
+ commit();
+ }
+ }
+
+ /**
+ * Checks the buffered mode of this Field.
+ *
+ * @return true if buffered mode is on, false otherwise
+ */
+ @Override
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ /* Property interface implementation */
+
+ /**
+ * Returns the (field) value converted to a String using toString().
+ *
+ * @see java.lang.Object#toString()
+ * @deprecated Instead use {@link #getValue()} to get the value of the
+ * field, {@link #getConvertedValue()} to get the field value
+ * converted to the data model type or
+ * {@link #getPropertyDataSource()} .getValue() to get the value
+ * of the data source.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ logger.warning("You are using AbstractField.toString() to get the value for a "
+ + getClass().getSimpleName()
+ + ". This is not recommended and will not be supported in future versions.");
+ final Object value = getFieldValue();
+ if (value == null) {
+ return null;
+ }
+ return value.toString();
+ }
+
+ /**
+ * Gets the current value of the field.
+ *
+ * <p>
+ * This is the visible, modified and possible invalid value the user have
+ * entered to the field.
+ * </p>
+ *
+ * <p>
+ * Note that the object returned is compatible with getType(). For example,
+ * if the type is String, this returns Strings even when the underlying
+ * datasource is of some other type. In order to access the converted value,
+ * use {@link #getConvertedValue()} and to access the value of the property
+ * data source, use {@link Property#getValue()} for the property data
+ * source.
+ * </p>
+ *
+ * <p>
+ * Since Vaadin 7.0, no implicit conversions between other data types and
+ * String are performed, but a converter is used if set.
+ * </p>
+ *
+ * @return the current value of the field.
+ */
+ @Override
+ public T getValue() {
+ return getFieldValue();
+ }
+
+ /**
+ * Sets the value of the field.
+ *
+ * @param newFieldValue
+ * the New value of the field.
+ * @throws Property.ReadOnlyException
+ */
+ @Override
+ public void setValue(Object newFieldValue)
+ throws Property.ReadOnlyException, Converter.ConversionException {
+ // This check is needed as long as setValue accepts Object instead of T
+ if (newFieldValue != null) {
+ if (!getType().isAssignableFrom(newFieldValue.getClass())) {
+ throw new Converter.ConversionException("Value of type "
+ + newFieldValue.getClass() + " cannot be assigned to "
+ + getType().getName());
+ }
+ }
+ setValue((T) newFieldValue, false);
+ }
+
+ /**
+ * Sets the value of the field.
+ *
+ * @param newFieldValue
+ * the New value of the field.
+ * @param repaintIsNotNeeded
+ * True iff caller is sure that repaint is not needed.
+ * @throws Property.ReadOnlyException
+ */
+ protected void setValue(T newFieldValue, boolean repaintIsNotNeeded)
+ throws Property.ReadOnlyException, Converter.ConversionException,
+ InvalidValueException {
+
+ if (!equals(newFieldValue, getInternalValue())) {
+
+ // Read only fields can not be changed
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Repaint is needed even when the client thinks that it knows the
+ // new state if validity of the component may change
+ if (repaintIsNotNeeded
+ && (isRequired() || getValidators() != null || getConverter() != null)) {
+ repaintIsNotNeeded = false;
+ }
+
+ if (!isInvalidAllowed()) {
+ /*
+ * If invalid values are not allowed the value must be validated
+ * before it is set. If validation fails, the
+ * InvalidValueException is thrown and the internal value is not
+ * updated.
+ */
+ validate(newFieldValue);
+ }
+
+ // Changes the value
+ setInternalValue(newFieldValue);
+ setModified(dataSource != null);
+
+ valueWasModifiedByDataSourceDuringCommit = false;
+ // In not buffering, try to commit
+ if (!isBuffered() && dataSource != null
+ && (isInvalidCommitted() || isValid())) {
+ try {
+
+ // Commits the value to datasource
+ committingValueToDataSource = true;
+ getPropertyDataSource().setValue(
+ convertToModel(newFieldValue));
+
+ // The buffer is now unmodified
+ setModified(false);
+
+ } catch (final Throwable e) {
+
+ // Sets the buffering state
+ currentBufferedSourceException = new Buffered.SourceException(
+ this, e);
+ markAsDirty();
+
+ // Throws the source exception
+ throw currentBufferedSourceException;
+ } finally {
+ committingValueToDataSource = false;
+ }
+ }
+
+ // If successful, remove set the buffering state to be ok
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+
+ if (valueWasModifiedByDataSourceDuringCommit) {
+ /*
+ * Value was modified by datasource. Force repaint even if
+ * repaint was not requested.
+ */
+ valueWasModifiedByDataSourceDuringCommit = repaintIsNotNeeded = false;
+ }
+
+ // Fires the value change
+ fireValueChange(repaintIsNotNeeded);
+
+ }
+ }
+
+ private static boolean equals(Object value1, Object value2) {
+ if (value1 == null) {
+ return value2 == null;
+ }
+ return value1.equals(value2);
+ }
+
+ /* External data source */
+
+ /**
+ * Gets the current data source of the field, if any.
+ *
+ * @return the current data source as a Property, or <code>null</code> if
+ * none defined.
+ */
+ @Override
+ public Property getPropertyDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * <p>
+ * Sets the specified Property as the data source for the field. All
+ * uncommitted changes are replaced with a value from the new data source.
+ * </p>
+ *
+ * <p>
+ * If the datasource has any validators, the same validators are added to
+ * the field. Because the default behavior of the field is to allow invalid
+ * values, but not to allow committing them, this only adds visual error
+ * messages to fields and do not allow committing them as long as the value
+ * is invalid. After the value is valid, the error message is not shown and
+ * the commit can be done normally.
+ * </p>
+ *
+ * <p>
+ * If the data source implements
+ * {@link com.vaadin.data.Property.ValueChangeNotifier} and/or
+ * {@link com.vaadin.data.Property.ReadOnlyStatusChangeNotifier}, the field
+ * registers itself as a listener and updates itself according to the events
+ * it receives. To avoid memory leaks caused by references to a field no
+ * longer in use, the listener registrations are removed on
+ * {@link AbstractField#detach() detach} and re-added on
+ * {@link AbstractField#attach() attach}.
+ * </p>
+ *
+ * <p>
+ * Note: before 6.5 we actually called discard() method in the beginning of
+ * the method. This was removed to simplify implementation, avoid excess
+ * calls to backing property and to avoid odd value change events that were
+ * previously fired (developer expects 0-1 value change events if this
+ * method is called). Some complex field implementations might now need to
+ * override this method to do housekeeping similar to discard().
+ * </p>
+ *
+ * @param newDataSource
+ * the new data source Property.
+ */
+ @Override
+ public void setPropertyDataSource(Property newDataSource) {
+
+ // Saves the old value
+ final Object oldValue = getInternalValue();
+
+ // Stop listening to the old data source
+ removePropertyListeners();
+
+ // Sets the new data source
+ dataSource = newDataSource;
+ getState().propertyReadOnly = dataSource == null ? false : dataSource
+ .isReadOnly();
+
+ // Check if the current converter is compatible.
+ if (newDataSource != null
+ && !ConverterUtil.canConverterHandle(getConverter(), getType(),
+ newDataSource.getType())) {
+ // Changing from e.g. Number -> Double should set a new converter,
+ // changing from Double -> Number can keep the old one (Property
+ // accepts Number)
+
+ // Set a new converter if there is a new data source and
+ // there is no old converter or the old is incompatible.
+ setConverter(newDataSource.getType());
+ }
+ // Gets the value from source
+ try {
+ if (dataSource != null) {
+ T fieldValue = convertFromDataSource(getDataSourceValue());
+ setInternalValue(fieldValue);
+ }
+ setModified(false);
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+ } catch (final Throwable e) {
+ setCurrentBufferedSourceException(new Buffered.SourceException(
+ this, e));
+ setModified(true);
+ }
+
+ // Listen to new data source if possible
+ addPropertyListeners();
+
+ // Copy the validators from the data source
+ if (dataSource instanceof Validatable) {
+ final Collection<Validator> validators = ((Validatable) dataSource)
+ .getValidators();
+ if (validators != null) {
+ for (final Iterator<Validator> i = validators.iterator(); i
+ .hasNext();) {
+ addValidator(i.next());
+ }
+ }
+ }
+
+ // Fires value change if the value has changed
+ T value = getInternalValue();
+ if ((value != oldValue)
+ && ((value != null && !value.equals(oldValue)) || value == null)) {
+ fireValueChange(false);
+ }
+ }
+
+ /**
+ * Retrieves a converter for the field from the converter factory defined
+ * for the application. Clears the converter if no application reference is
+ * available or if the factory returns null.
+ *
+ * @param datamodelType
+ * The type of the data model that we want to be able to convert
+ * from
+ */
+ public void setConverter(Class<?> datamodelType) {
+ Converter<T, ?> c = (Converter<T, ?>) ConverterUtil.getConverter(
+ getType(), datamodelType, getSession());
+ setConverter(c);
+ }
+
+ /**
+ * Convert the given value from the data source type to the UI type.
+ *
+ * @param newValue
+ * The data source value to convert.
+ * @return The converted value that is compatible with the UI type or the
+ * original value if its type is compatible and no converter is set.
+ * @throws Converter.ConversionException
+ * if there is no converter and the type is not compatible with
+ * the data source type.
+ */
+ private T convertFromDataSource(Object newValue) {
+ return ConverterUtil.convertFromModel(newValue, getType(),
+ getConverter(), getLocale());
+ }
+
+ /**
+ * Convert the given value from the UI type to the data source type.
+ *
+ * @param fieldValue
+ * The value to convert. Typically returned by
+ * {@link #getFieldValue()}
+ * @return The converted value that is compatible with the data source type.
+ * @throws Converter.ConversionException
+ * if there is no converter and the type is not compatible with
+ * the data source type.
+ */
+ private Object convertToModel(T fieldValue)
+ throws Converter.ConversionException {
+ try {
+ Class<?> modelType = null;
+ Property pd = getPropertyDataSource();
+ if (pd != null) {
+ modelType = pd.getType();
+ } else if (getConverter() != null) {
+ modelType = getConverter().getModelType();
+ }
+ return ConverterUtil.convertToModel(fieldValue,
+ (Class<Object>) modelType, getConverter(), getLocale());
+ } catch (ConversionException e) {
+ throw new ConversionException(
+ getConversionError(converter.getModelType()), e);
+ }
+ }
+
+ /**
+ * Returns the conversion error with {0} replaced by the data source type.
+ *
+ * @param dataSourceType
+ * The type of the data source
+ * @return The value conversion error string with parameters replaced.
+ */
+ protected String getConversionError(Class<?> dataSourceType) {
+ if (dataSourceType == null) {
+ return getConversionError();
+ } else {
+ return getConversionError().replace("{0}",
+ dataSourceType.getSimpleName());
+ }
+ }
+
+ /**
+ * Returns the current value (as returned by {@link #getValue()}) converted
+ * to the data source type.
+ * <p>
+ * This returns the same as {@link AbstractField#getValue()} if no converter
+ * has been set. The value is not necessarily the same as the data source
+ * value e.g. if the field is in buffered mode and has been modified.
+ * </p>
+ *
+ * @return The converted value that is compatible with the data source type
+ */
+ public Object getConvertedValue() {
+ return convertToModel(getFieldValue());
+ }
+
+ /**
+ * Sets the value of the field using a value of the data source type. The
+ * value given is converted to the field type and then assigned to the
+ * field. This will update the property data source in the same way as when
+ * {@link #setValue(Object)} is called.
+ *
+ * @param value
+ * The value to set. Must be the same type as the data source.
+ */
+ public void setConvertedValue(Object value) {
+ setValue(convertFromDataSource(value));
+ }
+
+ /* Validation */
+
+ /**
+ * Adds a new validator for the field's value. All validators added to a
+ * field are checked each time the its value changes.
+ *
+ * @param validator
+ * the new validator to be added.
+ */
+ @Override
+ public void addValidator(Validator validator) {
+ if (validators == null) {
+ validators = new LinkedList<Validator>();
+ }
+ validators.add(validator);
+ markAsDirty();
+ }
+
+ /**
+ * Gets the validators of the field.
+ *
+ * @return the Unmodifiable collection that holds all validators for the
+ * field.
+ */
+ @Override
+ public Collection<Validator> getValidators() {
+ if (validators == null || validators.isEmpty()) {
+ return null;
+ }
+ return Collections.unmodifiableCollection(validators);
+ }
+
+ /**
+ * Removes the validator from the field.
+ *
+ * @param validator
+ * the validator to remove.
+ */
+ @Override
+ public void removeValidator(Validator validator) {
+ if (validators != null) {
+ validators.remove(validator);
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Removes all validators from the field.
+ */
+ public void removeAllValidators() {
+ if (validators != null) {
+ validators.clear();
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Tests the current value against registered validators if the field is not
+ * empty. If the field is empty it is considered valid if it is not required
+ * and invalid otherwise. Validators are never checked for empty fields.
+ *
+ * In most cases, {@link #validate()} should be used instead of
+ * {@link #isValid()} to also get the error message.
+ *
+ * @return <code>true</code> if all registered validators claim that the
+ * current value is valid or if the field is empty and not required,
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean isValid() {
+
+ try {
+ validate();
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks the validity of the Field.
+ *
+ * A field is invalid if it is set as required (using
+ * {@link #setRequired(boolean)} and is empty, if one or several of the
+ * validators added to the field indicate it is invalid or if the value
+ * cannot be converted provided a converter has been set.
+ *
+ * The "required" validation is a built-in validation feature. If the field
+ * is required and empty this method throws an EmptyValueException with the
+ * error message set using {@link #setRequiredError(String)}.
+ *
+ * @see com.vaadin.data.Validatable#validate()
+ */
+ @Override
+ public void validate() throws Validator.InvalidValueException {
+
+ if (isRequired() && isEmpty()) {
+ throw new Validator.EmptyValueException(requiredError);
+ }
+ validate(getFieldValue());
+ }
+
+ /**
+ * Validates that the given value pass the validators for the field.
+ * <p>
+ * This method does not check the requiredness of the field.
+ *
+ * @param fieldValue
+ * The value to check
+ * @throws Validator.InvalidValueException
+ * if one or several validators fail
+ */
+ protected void validate(T fieldValue)
+ throws Validator.InvalidValueException {
+
+ Object valueToValidate = fieldValue;
+
+ // If there is a converter we start by converting the value as we want
+ // to validate the converted value
+ if (getConverter() != null) {
+ try {
+ valueToValidate = getConverter().convertToModel(fieldValue,
+ getLocale());
+ } catch (Exception e) {
+ throw new InvalidValueException(
+ getConversionError(getConverter().getModelType()));
+ }
+ }
+
+ List<InvalidValueException> validationExceptions = new ArrayList<InvalidValueException>();
+ if (validators != null) {
+ // Gets all the validation errors
+ for (Validator v : validators) {
+ try {
+ v.validate(valueToValidate);
+ } catch (final Validator.InvalidValueException e) {
+ validationExceptions.add(e);
+ }
+ }
+ }
+
+ // If there were no errors
+ if (validationExceptions.isEmpty()) {
+ return;
+ }
+
+ // If only one error occurred, throw it forwards
+ if (validationExceptions.size() == 1) {
+ throw validationExceptions.get(0);
+ }
+
+ InvalidValueException[] exceptionArray = validationExceptions
+ .toArray(new InvalidValueException[validationExceptions.size()]);
+
+ // Create a composite validator and include all exceptions
+ throw new Validator.InvalidValueException(null, exceptionArray);
+ }
+
+ /**
+ * Fields allow invalid values by default. In most cases this is wanted,
+ * because the field otherwise visually forget the user input immediately.
+ *
+ * @return true iff the invalid values are allowed.
+ * @see com.vaadin.data.Validatable#isInvalidAllowed()
+ */
+ @Override
+ public boolean isInvalidAllowed() {
+ return invalidAllowed;
+ }
+
+ /**
+ * Fields allow invalid values by default. In most cases this is wanted,
+ * because the field otherwise visually forget the user input immediately.
+ * <p>
+ * In common setting where the user wants to assure the correctness of the
+ * datasource, but allow temporarily invalid contents in the field, the user
+ * should add the validators to datasource, that should not allow invalid
+ * values. The validators are automatically copied to the field when the
+ * datasource is set.
+ * </p>
+ *
+ * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean)
+ */
+ @Override
+ public void setInvalidAllowed(boolean invalidAllowed)
+ throws UnsupportedOperationException {
+ this.invalidAllowed = invalidAllowed;
+ }
+
+ /**
+ * Error messages shown by the fields are composites of the error message
+ * thrown by the superclasses (that is the component error message),
+ * validation errors and buffered source errors.
+ *
+ * @see com.vaadin.ui.AbstractComponent#getErrorMessage()
+ */
+ @Override
+ public ErrorMessage getErrorMessage() {
+
+ /*
+ * Check validation errors only if automatic validation is enabled.
+ * Empty, required fields will generate a validation error containing
+ * the requiredError string. For these fields the exclamation mark will
+ * be hidden but the error must still be sent to the client.
+ */
+ Validator.InvalidValueException validationError = null;
+ if (isValidationVisible()) {
+ try {
+ validate();
+ } catch (Validator.InvalidValueException e) {
+ if (!e.isInvisible()) {
+ validationError = e;
+ }
+ }
+ }
+
+ // Check if there are any systems errors
+ final ErrorMessage superError = super.getErrorMessage();
+
+ // Return if there are no errors at all
+ if (superError == null && validationError == null
+ && getCurrentBufferedSourceException() == null) {
+ return null;
+ }
+
+ // Throw combination of the error types
+ return new CompositeErrorMessage(
+ new ErrorMessage[] {
+ superError,
+ AbstractErrorMessage
+ .getErrorMessageForException(validationError),
+ AbstractErrorMessage
+ .getErrorMessageForException(getCurrentBufferedSourceException()) });
+
+ }
+
+ /* Value change events */
+
+ private static final Method VALUE_CHANGE_METHOD;
+
+ static {
+ try {
+ VALUE_CHANGE_METHOD = Property.ValueChangeListener.class
+ .getDeclaredMethod("valueChange",
+ new Class[] { Property.ValueChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in AbstractField");
+ }
+ }
+
+ /*
+ * Adds a value change listener for the field. Don't add a JavaDoc comment
+ * here, we use the default documentation from the implemented interface.
+ */
+ @Override
+ public void addValueChangeListener(Property.ValueChangeListener listener) {
+ addListener(AbstractField.ValueChangeEvent.class, listener,
+ VALUE_CHANGE_METHOD);
+ }
+
+ /**
+ * @deprecated Since 7.0, replaced by
+ * {@link #addValueChangeListener(com.vaadin.data.Property.ValueChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(Property.ValueChangeListener listener) {
+ addValueChangeListener(listener);
+ }
+
+ /*
+ * Removes a value change listener from the field. Don't add a JavaDoc
+ * comment here, we use the default documentation from the implemented
+ * interface.
+ */
+ @Override
+ public void removeValueChangeListener(Property.ValueChangeListener listener) {
+ removeListener(AbstractField.ValueChangeEvent.class, listener,
+ VALUE_CHANGE_METHOD);
+ }
+
+ /**
+ * @deprecated Since 7.0, replaced by
+ * {@link #removeValueChangeListener(com.vaadin.data.Property.ValueChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(Property.ValueChangeListener listener) {
+ removeValueChangeListener(listener);
+ }
+
+ /**
+ * Emits the value change event. The value contained in the field is
+ * validated before the event is created.
+ */
+ protected void fireValueChange(boolean repaintIsNotNeeded) {
+ fireEvent(new AbstractField.ValueChangeEvent(this));
+ if (!repaintIsNotNeeded) {
+ markAsDirty();
+ }
+ }
+
+ /* Read-only status change events */
+
+ private static final Method READ_ONLY_STATUS_CHANGE_METHOD;
+
+ static {
+ try {
+ READ_ONLY_STATUS_CHANGE_METHOD = Property.ReadOnlyStatusChangeListener.class
+ .getDeclaredMethod(
+ "readOnlyStatusChange",
+ new Class[] { Property.ReadOnlyStatusChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in AbstractField");
+ }
+ }
+
+ /**
+ * React to read only status changes of the property by requesting a
+ * repaint.
+ *
+ * @see Property.ReadOnlyStatusChangeListener
+ */
+ @Override
+ public void readOnlyStatusChange(Property.ReadOnlyStatusChangeEvent event) {
+ getState().propertyReadOnly = event.getProperty().isReadOnly();
+ }
+
+ /**
+ * An <code>Event</code> object specifying the Property whose read-only
+ * status has changed.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public static class ReadOnlyStatusChangeEvent extends Component.Event
+ implements Property.ReadOnlyStatusChangeEvent, Serializable {
+
+ /**
+ * New instance of text change event.
+ *
+ * @param source
+ * the Source of the event.
+ */
+ public ReadOnlyStatusChangeEvent(AbstractField source) {
+ super(source);
+ }
+
+ /**
+ * Property where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+ }
+
+ /*
+ * Adds a read-only status change listener for the field. Don't add a
+ * JavaDoc comment here, we use the default documentation from the
+ * implemented interface.
+ */
+ @Override
+ public void addReadOnlyStatusChangeListener(
+ Property.ReadOnlyStatusChangeListener listener) {
+ addListener(Property.ReadOnlyStatusChangeEvent.class, listener,
+ READ_ONLY_STATUS_CHANGE_METHOD);
+ }
+
+ /**
+ * @deprecated Since 7.0, replaced by
+ * {@link #addReadOnlyStatusChangeListener(com.vaadin.data.Property.ReadOnlyStatusChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(Property.ReadOnlyStatusChangeListener listener) {
+ addReadOnlyStatusChangeListener(listener);
+ }
+
+ /*
+ * Removes a read-only status change listener from the field. Don't add a
+ * JavaDoc comment here, we use the default documentation from the
+ * implemented interface.
+ */
+ @Override
+ public void removeReadOnlyStatusChangeListener(
+ Property.ReadOnlyStatusChangeListener listener) {
+ removeListener(Property.ReadOnlyStatusChangeEvent.class, listener,
+ READ_ONLY_STATUS_CHANGE_METHOD);
+ }
+
+ /**
+ * @deprecated Since 7.0, replaced by
+ * {@link #removeReadOnlyStatusChangeListener(com.vaadin.data.Property.ReadOnlyStatusChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(Property.ReadOnlyStatusChangeListener listener) {
+ removeReadOnlyStatusChangeListener(listener);
+ }
+
+ /**
+ * Emits the read-only status change event. The value contained in the field
+ * is validated before the event is created.
+ */
+ protected void fireReadOnlyStatusChange() {
+ fireEvent(new AbstractField.ReadOnlyStatusChangeEvent(this));
+ }
+
+ /**
+ * This method listens to data source value changes and passes the changes
+ * forwards.
+ *
+ * Changes are not forwarded to the listeners of the field during internal
+ * operations of the field to avoid duplicate notifications.
+ *
+ * @param event
+ * the value change event telling the data source contents have
+ * changed.
+ */
+ @Override
+ public void valueChange(Property.ValueChangeEvent event) {
+ if (!isBuffered()) {
+ if (committingValueToDataSource) {
+ boolean propertyNotifiesOfTheBufferedValue = equals(event
+ .getProperty().getValue(), getInternalValue());
+ if (!propertyNotifiesOfTheBufferedValue) {
+ /*
+ * Property (or chained property like PropertyFormatter) now
+ * reports different value than the one the field has just
+ * committed to it. In this case we respect the property
+ * value.
+ *
+ * Still, we don't fire value change yet, but instead
+ * postpone it until "commit" is done. See setValue(Object,
+ * boolean) and commit().
+ */
+ readValueFromProperty(event);
+ valueWasModifiedByDataSourceDuringCommit = true;
+ }
+ } else if (!isModified()) {
+ readValueFromProperty(event);
+ fireValueChange(false);
+ }
+ }
+ }
+
+ private void readValueFromProperty(Property.ValueChangeEvent event) {
+ setInternalValue(convertFromDataSource(event.getProperty().getValue()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void focus() {
+ super.focus();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Focusable#getTabIndex()
+ */
+ @Override
+ public int getTabIndex() {
+ return getState().tabIndex;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
+ */
+ @Override
+ public void setTabIndex(int tabIndex) {
+ getState().tabIndex = tabIndex;
+ }
+
+ /**
+ * Returns the internal field value, which might not match the data source
+ * value e.g. if the field has been modified and is not in write-through
+ * mode.
+ *
+ * This method can be overridden by subclasses together with
+ * {@link #setInternalValue(Object)} to compute internal field value at
+ * runtime. When doing so, typically also {@link #isModified()} needs to be
+ * overridden and care should be taken in the management of the empty state
+ * and buffering support.
+ *
+ * @return internal field value
+ */
+ protected T getInternalValue() {
+ return value;
+ }
+
+ /**
+ * Sets the internal field value. This is purely used by AbstractField to
+ * change the internal Field value. It does not trigger valuechange events.
+ * It can be overridden by the inheriting classes to update all dependent
+ * variables.
+ *
+ * Subclasses can also override {@link #getInternalValue()} if necessary.
+ *
+ * @param newValue
+ * the new value to be set.
+ */
+ protected void setInternalValue(T newValue) {
+ value = newValue;
+ if (validators != null && !validators.isEmpty()) {
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.Component#attach()
+ */
+ @Override
+ public void attach() {
+ super.attach();
+
+ if (!isListeningToPropertyEvents) {
+ addPropertyListeners();
+ if (!isModified() && !isBuffered()) {
+ // Update value from data source
++ updateValueFromDataSource();
+ }
+ }
+ }
+
+ @Override
+ public void detach() {
+ super.detach();
+ // Stop listening to data source events on detach to avoid a potential
+ // memory leak. See #6155.
+ removePropertyListeners();
+ }
+
+ /**
+ * Is this field required. Required fields must filled by the user.
+ *
+ * If the field is required, it is visually indicated in the user interface.
+ * Furthermore, setting field to be required implicitly adds "non-empty"
+ * validator and thus isValid() == false or any isEmpty() fields. In those
+ * cases validation errors are not painted as it is obvious that the user
+ * must fill in the required fields.
+ *
+ * On the other hand, for the non-required fields isValid() == true if the
+ * field isEmpty() regardless of any attached validators.
+ *
+ *
+ * @return <code>true</code> if the field is required, otherwise
+ * <code>false</code>.
+ */
+ @Override
+ public boolean isRequired() {
+ return getState().required;
+ }
+
+ /**
+ * Sets the field required. Required fields must filled by the user.
+ *
+ * If the field is required, it is visually indicated in the user interface.
+ * Furthermore, setting field to be required implicitly adds "non-empty"
+ * validator and thus isValid() == false or any isEmpty() fields. In those
+ * cases validation errors are not painted as it is obvious that the user
+ * must fill in the required fields.
+ *
+ * On the other hand, for the non-required fields isValid() == true if the
+ * field isEmpty() regardless of any attached validators.
+ *
+ * @param required
+ * Is the field required.
+ */
+ @Override
+ public void setRequired(boolean required) {
+ getState().required = required;
+ }
+
+ /**
+ * Set the error that is show if this field is required, but empty. When
+ * setting requiredMessage to be "" or null, no error pop-up or exclamation
+ * mark is shown for a empty required field. This faults to "". Even in
+ * those cases isValid() returns false for empty required fields.
+ *
+ * @param requiredMessage
+ * Message to be shown when this field is required, but empty.
+ */
+ @Override
+ public void setRequiredError(String requiredMessage) {
+ requiredError = requiredMessage;
+ markAsDirty();
+ }
+
+ @Override
+ public String getRequiredError() {
+ return requiredError;
+ }
+
+ /**
+ * Gets the error that is shown if the field value cannot be converted to
+ * the data source type.
+ *
+ * @return The error that is shown if conversion of the field value fails
+ */
+ public String getConversionError() {
+ return conversionError;
+ }
+
+ /**
+ * Sets the error that is shown if the field value cannot be converted to
+ * the data source type. If {0} is present in the message, it will be
+ * replaced by the simple name of the data source type.
+ *
+ * @param valueConversionError
+ * Message to be shown when conversion of the value fails
+ */
+ public void setConversionError(String valueConversionError) {
+ this.conversionError = valueConversionError;
+ markAsDirty();
+ }
+
+ /**
+ * Is the field empty?
+ *
+ * In general, "empty" state is same as null. As an exception, TextField
+ * also treats empty string as "empty".
+ */
+ protected boolean isEmpty() {
+ return (getFieldValue() == null);
+ }
+
+ /**
+ * Is automatic, visible validation enabled?
+ *
+ * If automatic validation is enabled, any validators connected to this
+ * component are evaluated while painting the component and potential error
+ * messages are sent to client. If the automatic validation is turned off,
+ * isValid() and validate() methods still work, but one must show the
+ * validation in their own code.
+ *
+ * @return True, if automatic validation is enabled.
+ */
+ public boolean isValidationVisible() {
+ return validationVisible;
+ }
+
+ /**
+ * Enable or disable automatic, visible validation.
+ *
+ * If automatic validation is enabled, any validators connected to this
+ * component are evaluated while painting the component and potential error
+ * messages are sent to client. If the automatic validation is turned off,
+ * isValid() and validate() methods still work, but one must show the
+ * validation in their own code.
+ *
+ * @param validateAutomatically
+ * True, if automatic validation is enabled.
+ */
+ public void setValidationVisible(boolean validateAutomatically) {
+ if (validationVisible != validateAutomatically) {
+ markAsDirty();
+ validationVisible = validateAutomatically;
+ }
+ }
+
+ /**
+ * Sets the current buffered source exception.
+ *
+ * @param currentBufferedSourceException
+ */
+ public void setCurrentBufferedSourceException(
+ Buffered.SourceException currentBufferedSourceException) {
+ this.currentBufferedSourceException = currentBufferedSourceException;
+ markAsDirty();
+ }
+
+ /**
+ * Gets the current buffered source exception.
+ *
+ * @return The current source exception
+ */
+ protected Buffered.SourceException getCurrentBufferedSourceException() {
+ return currentBufferedSourceException;
+ }
+
+ /**
+ * A ready-made {@link ShortcutListener} that focuses the given
+ * {@link Focusable} (usually a {@link Field}) when the keyboard shortcut is
+ * invoked.
+ *
+ */
+ public static class FocusShortcut extends ShortcutListener {
+ protected Focusable focusable;
+
+ /**
+ * Creates a keyboard shortcut for focusing the given {@link Focusable}
+ * using the shorthand notation defined in {@link ShortcutAction}.
+ *
+ * @param focusable
+ * to focused when the shortcut is invoked
+ * @param shorthandCaption
+ * caption with keycode and modifiers indicated
+ */
+ public FocusShortcut(Focusable focusable, String shorthandCaption) {
+ super(shorthandCaption);
+ this.focusable = focusable;
+ }
+
+ /**
+ * Creates a keyboard shortcut for focusing the given {@link Focusable}.
+ *
+ * @param focusable
+ * to focused when the shortcut is invoked
+ * @param keyCode
+ * keycode that invokes the shortcut
+ * @param modifiers
+ * modifiers required to invoke the shortcut
+ */
+ public FocusShortcut(Focusable focusable, int keyCode, int... modifiers) {
+ super(null, keyCode, modifiers);
+ this.focusable = focusable;
+ }
+
+ /**
+ * Creates a keyboard shortcut for focusing the given {@link Focusable}.
+ *
+ * @param focusable
+ * to focused when the shortcut is invoked
+ * @param keyCode
+ * keycode that invokes the shortcut
+ */
+ public FocusShortcut(Focusable focusable, int keyCode) {
+ this(focusable, keyCode, null);
+ }
+
+ @Override
+ public void handleAction(Object sender, Object target) {
+ focusable.focus();
+ }
+ }
+
++ private void updateValueFromDataSource() {
++ if (dataSource != null) {
++
++ // Gets the correct value from datasource
++ T newFieldValue;
++ try {
++
++ // Discards buffer by overwriting from datasource
++ newFieldValue = convertFromDataSource(getDataSourceValue());
++
++ // If successful, remove set the buffering state to be ok
++ if (getCurrentBufferedSourceException() != null) {
++ setCurrentBufferedSourceException(null);
++ }
++ } catch (final Throwable e) {
++ // FIXME: What should really be done here if conversion fails?
++
++ // Sets the buffering state
++ currentBufferedSourceException = new Buffered.SourceException(
++ this, e);
++ markAsDirty();
++
++ // Throws the source exception
++ throw currentBufferedSourceException;
++ }
++
++ final boolean wasModified = isModified();
++ setModified(false);
++
++ // If the new value differs from the previous one
++ if (!equals(newFieldValue, getInternalValue())) {
++ setInternalValue(newFieldValue);
++ fireValueChange(false);
++ } else if (wasModified) {
++ // If the value did not change, but the modification status did
++ markAsDirty();
++ }
++ }
++ }
++
+ /**
+ * Gets the converter used to convert the property data source value to the
+ * field value.
+ *
+ * @return The converter or null if none is set.
+ */
+ public Converter<T, Object> getConverter() {
+ return converter;
+ }
+
+ /**
+ * Sets the converter used to convert the field value to property data
+ * source type. The converter must have a presentation type that matches the
+ * field type.
+ *
+ * @param converter
+ * The new converter to use.
+ */
+ public void setConverter(Converter<T, ?> converter) {
+ this.converter = (Converter<T, Object>) converter;
+ markAsDirty();
+ }
+
+ @Override
+ protected AbstractFieldState getState() {
+ return (AbstractFieldState) super.getState();
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ super.beforeClientResponse(initial);
+
+ // Hide the error indicator if needed
+ getState().hideErrors = shouldHideErrors();
+ }
+
+ /**
+ * Registers this as an event listener for events sent by the data source
+ * (if any). Does nothing if
+ * <code>isListeningToPropertyEvents == true</code>.
+ */
+ private void addPropertyListeners() {
+ if (!isListeningToPropertyEvents) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource).addListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .addListener(this);
+ }
+ isListeningToPropertyEvents = true;
+ }
+ }
+
+ /**
+ * Stops listening to events sent by the data source (if any). Does nothing
+ * if <code>isListeningToPropertyEvents == false</code>.
+ */
+ private void removePropertyListeners() {
+ if (isListeningToPropertyEvents) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ isListeningToPropertyEvents = false;
+ }
+ }
+}
--- /dev/null
--- /dev/null
++<?xml version="1.0" encoding="UTF-8"?>
++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
++<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
++<head profile="http://selenium-ide.openqa.org/profiles/test-case">
++<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
++<link rel="selenium.base" href="" />
++<title>DateFieldExtendedRange</title>
++</head>
++<body>
++<table cellpadding="1" cellspacing="1" border="1">
++<thead>
++<tr><td rowspan="1" colspan="3">DateFieldExtendedRange</td></tr>
++</thead><tbody>
++<tr>
++ <td>open</td>
++ <td>/run/DateFieldExtendedRange?restartApplication</td>
++ <td></td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#popupButton</td>
++ <td>8,16</td>
++</tr>
++<tr>
++ <td>screenCapture</td>
++ <td></td>
++ <td>2011-01-01</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>27</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]#day37</td>
++ <td>6</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#popupButton</td>
++ <td>11,11</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#popupButton</td>
++ <td>10,12</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>26</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]#day36</td>
++ <td>5</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#popupButton</td>
++ <td>15,20</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#popupButton</td>
++ <td>11,8</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>27</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]#day37</td>
++ <td>6</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>//div[@id='runDateFieldExtendedRange-312092003']/div/div[2]/div/div[2]/div/div/div/div[1]/div/div/div/div/div[2]/div[2]/div/table/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>27</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]#day37</td>
++ <td>6</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>//div[@id='runDateFieldExtendedRange-312092003']/div/div[2]/div/div[2]/div/div/div/div[1]/div/div/div/div/div[4]/div[2]/div/table/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>26</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]#day36</td>
++ <td>5</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>//div[@id='runDateFieldExtendedRange-312092003']/div/div[2]/div/div[2]/div/div/div/div[1]/div/div/div/div/div[6]/div[2]/div/table/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>27</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]#day37</td>
++ <td>6</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#popupButton</td>
++ <td>11,9</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>12,9</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#field</td>
++ <td>27.12.2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#popupButton</td>
++ <td>10,15</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>14,7</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#field</td>
++ <td>12/26/10</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#popupButton</td>
++ <td>15,10</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>6,11</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#field</td>
++ <td>27.12.2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//div[@id='runDateFieldExtendedRange-312092003']/div/div[2]/div/div[2]/div/div/div/div[1]/div/div/div/div/div[2]/div[2]/div/table/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>11,7</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>joulukuu 2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//div[@id='runDateFieldExtendedRange-312092003']/div/div[2]/div/div[2]/div/div/div/div[1]/div/div/div/div/div[4]/div[2]/div/table/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>13,4</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>December 2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//div[@id='runDateFieldExtendedRange-312092003']/div/div[2]/div/div[2]/div/div/div/div[1]/div/div/div/div/div[6]/div[2]/div/table/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>11,8</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>joulukuu 2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]#day27</td>
++ <td>13,13</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]</td>
++ <td>down</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>tammikuu 2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]#day26</td>
++ <td>12,7</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]</td>
++ <td>down</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>January 2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]#day27</td>
++ <td>14,12</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]</td>
++ <td>down</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>tammikuu 2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#popupButton</td>
++ <td>14,17</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]</td>
++ <td>down</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]</td>
++ <td>enter</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#field</td>
++ <td>3.1.2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#popupButton</td>
++ <td>9,12</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]</td>
++ <td>down</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]</td>
++ <td>enter</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#field</td>
++ <td>1/2/11</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#popupButton</td>
++ <td>15,15</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]</td>
++ <td>down</td>
++</tr>
++<tr>
++ <td>pressSpecialKey</td>
++ <td>vaadin=runDateFieldExtendedRange::Root/VOverlay[0]/VCalendarPanel[0]</td>
++ <td>enter</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#field</td>
++ <td>3.1.2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#popupButton</td>
++ <td>10,15</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>12,8</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#field</td>
++ <td>27.12.2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#popupButton</td>
++ <td>12,15</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>14,9</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[2]/VPopupCalendar[0]#field</td>
++ <td>12/26/10</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#popupButton</td>
++ <td>15,15</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>//table[@id='PID_VAADIN_POPUPCAL']/tbody/tr[2]/td/table/tbody/tr[2]/td[2]/span</td>
++ <td>8,12</td>
++</tr>
++<tr>
++ <td>assertValue</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[4]/VPopupCalendar[0]#field</td>
++ <td>27.12.2010</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]#day37</td>
++ <td>18,10</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[1]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>helmikuu 2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]#day36</td>
++ <td>15,11</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[3]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>February 2011</td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]#day37</td>
++ <td>19,13</td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[5]/VDateFieldCalendar[0]/VCalendarPanel[0]#header</td>
++ <td>helmikuu 2011</td>
++</tr>
++<tr>
++ <td>click</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[1]/VButton[0]/domChild[0]/domChild[0]</td>
++ <td></td>
++</tr>
++<tr>
++ <td>mouseClick</td>
++ <td>vaadin=runDateFieldExtendedRange::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VGridLayout[0]/AbsolutePanel[0]/ChildComponentContainer[0]/VPopupCalendar[0]#popupButton</td>
++ <td>10,13</td>
++</tr>
++<tr>
++ <td>screenCapture</td>
++ <td></td>
++ <td>2010-02-16</td>
++</tr>
++
++</tbody></table>
++</body>
++</html>
--- /dev/null
--- /dev/null
++package com.vaadin.tests.components.datefield;
++
++import java.util.Calendar;
++import java.util.Locale;
++
++import com.vaadin.tests.components.TestBase;
++import com.vaadin.ui.Button;
++import com.vaadin.ui.Button.ClickEvent;
++import com.vaadin.ui.Button.ClickListener;
++import com.vaadin.ui.DateField;
++import com.vaadin.ui.GridLayout;
++import com.vaadin.ui.InlineDateField;
++import com.vaadin.ui.PopupDateField;
++
++@SuppressWarnings("serial")
++public class DateFieldExtendedRange extends TestBase {
++
++ private Calendar date = Calendar.getInstance();
++
++ @Override
++ protected void setup() {
++ date.set(2011, 0, 1);
++
++ GridLayout layout = new GridLayout(2, 3);
++ layout.setWidth("600px");
++ layout.setSpacing(true);
++
++ final DateField[] fields = new DateField[6];
++
++ Locale fi = new Locale("fi", "FI");
++ Locale us = new Locale("en", "US");
++
++ fields[0] = makeDateField(true, fi, "Finnish locale");
++ fields[1] = makeDateField(false, fi, "Finnish locale");
++
++ fields[2] = makeDateField(true, us, "US English locale");
++ fields[3] = makeDateField(false, us, "US English locale");
++
++ fields[4] = makeDateField(true, fi, "Finnish locale with week numbers");
++ fields[4].setShowISOWeekNumbers(true);
++ fields[5] = makeDateField(false, fi, "Finnish locale with week numbers");
++ fields[5].setShowISOWeekNumbers(true);
++
++ for (DateField f : fields) {
++ layout.addComponent(f);
++ }
++
++ addComponent(layout);
++
++ addComponent(new Button("Change date", new ClickListener() {
++ public void buttonClick(ClickEvent event) {
++ date.set(2010, 1, 16);
++ for (DateField f : fields) {
++ f.setValue(date.getTime());
++ }
++ }
++ }));
++ }
++
++ @Override
++ protected String getDescription() {
++ return "Show a few days of the preceding and following months in the datefield popup";
++ }
++
++ @Override
++ protected Integer getTicketNumber() {
++ return 6718;
++ }
++
++ private DateField makeDateField(boolean isPopup, Locale locale,
++ String caption) {
++ DateField df = isPopup ? new PopupDateField() : new InlineDateField();
++ df.setResolution(DateField.RESOLUTION_DAY);
++ df.setValue(date.getTime());
++ df.setLocale(locale);
++ df.setCaption(caption);
++ return df;
++ }
++}
--- /dev/null
--- /dev/null
++<?xml version="1.0" encoding="UTF-8"?>
++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
++<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
++<head profile="http://selenium-ide.openqa.org/profiles/test-case">
++<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
++<link rel="selenium.base" href="" />
++<title>TableReduceContainerSize</title>
++</head>
++<body>
++<table cellpadding="1" cellspacing="1" border="1">
++<thead>
++<tr><td rowspan="1" colspan="3">TableReduceContainerSize</td></tr>
++</thead><tbody>
++<tr>
++ <td>open</td>
++ <td>/run/TableReduceContainerSize?restartApplication</td>
++ <td></td>
++</tr>
++<tr>
++ <td>scroll</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]</td>
++ <td>19700</td>
++</tr>
++<tr>
++ <td>pause</td>
++ <td>300</td>
++ <td></td>
++</tr>
++<tr>
++ <td>click</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[3]/VButton[0]/domChild[0]/domChild[0]</td>
++ <td></td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[1]/VLabel[0]</td>
++ <td>Index: *</td>
++</tr>
++<tr>
++ <td>click</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[3]/VButton[0]/domChild[0]/domChild[0]</td>
++ <td></td>
++</tr>
++<tr>
++ <td>scroll</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[0]/VScrollTable[0]/domChild[1]</td>
++ <td>19700</td>
++</tr>
++<tr>
++ <td>pause</td>
++ <td>300</td>
++ <td></td>
++</tr>
++<tr>
++ <td>click</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[2]/VButton[0]/domChild[0]/domChild[0]</td>
++ <td></td>
++</tr>
++<tr>
++ <td>assertText</td>
++ <td>vaadin=runTableReduceContainerSize::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VHorizontalLayout[0]/ChildComponentContainer[1]/VLabel[0]</td>
++ <td>Index: *</td>
++</tr>
++
++</tbody></table>
++</body>
++</html>
--- /dev/null
--- /dev/null
++package com.vaadin.tests.components.table;
++
++import java.util.ArrayList;
++import java.util.Date;
++import java.util.List;
++
++import com.vaadin.data.Container.Filter;
++import com.vaadin.data.Item;
++import com.vaadin.data.util.BeanItemContainer;
++import com.vaadin.tests.components.TestBase;
++import com.vaadin.ui.Button;
++import com.vaadin.ui.Button.ClickEvent;
++import com.vaadin.ui.HorizontalLayout;
++import com.vaadin.ui.Label;
++import com.vaadin.ui.Table;
++
++/**
++ * Test for #8291 and #7666: NegativeArraySizeException when Table scrolled to
++ * the end and its size reduced.
++ */
++public class TableReduceContainerSize extends TestBase {
++
++ @Override
++ protected void setup() {
++ addComponent(new TestView());
++ }
++
++ private static class DecimateFilter implements Filter {
++ public boolean passesFilter(Object itemId, Item item)
++ throws UnsupportedOperationException {
++ return ((((TestObject) itemId).property3 % 10) == 0);
++ }
++
++ public boolean appliesToProperty(Object propertyId) {
++ return true;
++ }
++ }
++
++ private static class TestView extends HorizontalLayout {
++
++ private Filter filter = null;
++
++ private boolean reduceData;
++
++ private TestView() {
++ final Table table = new Table();
++ List<TestObject> data = createData(1000);
++ final BeanItemContainer<TestObject> container = new BeanItemContainer<TestObject>(
++ TestObject.class, data) {
++
++ @Override
++ public int size() {
++ if (reduceData) {
++ return 100;
++ } else {
++ return super.size();
++ }
++ }
++ };
++ table.setContainerDataSource(container);
++ addComponent(table);
++ final Label label = new Label();
++ addComponent(label);
++ Button button = new Button("Click");
++ button.addListener(new Button.ClickListener() {
++ public void buttonClick(ClickEvent event) {
++ try {
++ reduceData = !reduceData;
++ table.refreshRowCache();
++ label.setValue("Index: "
++ + table.getCurrentPageFirstItemIndex());
++ } catch (Exception e) {
++ label.setValue("Exception: "
++ + e.getClass().getSimpleName());
++ }
++ }
++ });
++ addComponent(button);
++ Button button2 = new Button("Filter");
++ button2.addListener(new Button.ClickListener() {
++ public void buttonClick(ClickEvent event) {
++ try {
++ if (filter != null) {
++ container.removeAllContainerFilters();
++ filter = null;
++ } else {
++ filter = new DecimateFilter();
++ container.addContainerFilter(filter);
++ }
++ table.refreshRowCache();
++ label.setValue("Index: "
++ + table.getCurrentPageFirstItemIndex());
++ } catch (Exception e) {
++ label.setValue("Exception: "
++ + e.getClass().getSimpleName());
++ }
++ }
++ });
++ addComponent(button2);
++ }
++ }
++
++ private static List<TestObject> createData(int count) {
++ ArrayList<TestObject> data = new ArrayList<TestObject>(count);
++ for (int i = 0; i < count; i++) {
++ data.add(new TestObject("string-" + i, new Date(), i));
++ }
++ return data;
++ }
++
++ public static class TestObject {
++
++ private String property1;
++ private Date property2;
++ private Integer property3;
++
++ public TestObject(String property1, Date property2, Integer property3) {
++ this.property1 = property1;
++ this.property2 = property2;
++ this.property3 = property3;
++ }
++
++ public String getProperty1() {
++ return property1;
++ }
++
++ public Date getProperty2() {
++ return property2;
++ }
++
++ public Integer getProperty3() {
++ return property3;
++ }
++
++ }
++
++ @Override
++ protected String getDescription() {
++ return "Table throws NegativeArraySizeException if container size is reduced to less than current scroll position";
++ }
++
++ @Override
++ protected Integer getTicketNumber() {
++ return 8291;
++ }
++
++}